Skip to content

feat(release): tagged GitHub Release with SHA-256 and immutable GHCR - #107

Merged
seonghobae merged 10 commits into
feat/issue-80-optimistic-concurrencyfrom
feat/issue-84-signed-release
Aug 26, 2026
Merged

feat(release): tagged GitHub Release with SHA-256 and immutable GHCR#107
seonghobae merged 10 commits into
feat/issue-80-optimistic-concurrencyfrom
feat/issue-84-signed-release

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Issue #84 first slice. Does not re-implement #78#81 store slices, rustls, OCC, or outbox consumers.

A git tag vX.Y.Z runs .github/workflows/release.yml: locked cargo build --release, SHA256SUMS via scripts/release-checksums.sh, a GitHub Release, and an immutable ghcr.io/contextualwisdomlab/waf-ids-ai-soc:vX.Y.Z image (no moving latest). Promotion and rollback are tag-for-tag (docs/runbooks/release.md). Buyer evidence lists the runbook and workflow.

Stacked on #106 (feat/issue-81-outbox-consumers). Merge order: #95, then #96, then #97, then #98, then #99, then #105, then #106, then this PR. Org ruleset 18156473 still requires two independent approvals; do not --admin merge.

Tests

  • cargo fmt --check
  • cargo test --locked --workspace (includes release_checksums_script_emits_sha256_lines)
  • cargo clippy --locked --workspace --all-targets -- -D warnings
  • Two scripts/smoke.sh runs (/healthz + /admin, 2B KRW, evidence includes docs/runbooks/release.md)

Remaining on #84: keyless cosign / SBOM attestation on the same tag.


Open in Devin Review

Issue #84 first slice. A vX.Y.Z tag builds a locked binary, checksums,
a GitHub Release, and ghcr.io/contextualwisdomlab/waf-ids-ai-soc:vX.Y.Z
with no moving latest tag. Promotion and rollback are tag-for-tag.
Do not re-implement store slices or OCC.
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a895cde-7e5c-4e2e-a5f7-28f345841a86

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Open in Devin Review

Comment thread .github/workflows/release.yml
Comment thread .github/workflows/release.yml Outdated
Comment on lines +43 to +46
image="ghcr.io/contextualwisdomlab/waf-ids-ai-soc"
tag="${GITHUB_REF_NAME}"
docker build -t "${image}:${tag}" .
docker push "${image}:${tag}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: GHCR tag immutability is convention-only

The pushed image ghcr.io/contextualwisdomlab/waf-ids-ai-soc:vX.Y.Z is described as immutable, but GHCR does not enforce tag immutability by default. A later push to the same tag overwrites it; immutability rests only on the runbook convention, not registry protection.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged: GHCR tags are aliases. Promotion authority is the content digest plus signatures, not the tag. Keyless cosign-by-digest, SPDX SBOM, and SLSA attestations are on #108 (feat/issue-84-cosign-sbom). No moving latest.

Comment thread .github/workflows/release.yml Outdated
Comment on lines +21 to +46
- name: Build release binary
run: cargo build --locked --release
- name: Stage checksums
run: |
mkdir -p dist
cp target/release/waf-ids-ai-soc dist/waf-ids-ai-soc-linux-x86_64
scripts/release-checksums.sh dist/waf-ids-ai-soc-linux-x86_64 > dist/SHA256SUMS
cat dist/SHA256SUMS
- name: GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
dist/waf-ids-ai-soc-linux-x86_64 \
dist/SHA256SUMS \
--generate-notes \
--verify-tag
- name: Publish immutable GHCR image
env:
GH_TOKEN: ${{ github.token }}
run: |
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
image="ghcr.io/contextualwisdomlab/waf-ids-ai-soc"
tag="${GITHUB_REF_NAME}"
docker build -t "${image}:${tag}" .
docker push "${image}:${tag}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Release compiles the binary twice

The workflow runs cargo build --locked --release, then docker build, and the Dockerfile compiles the same release binary again in its build stage. The binary is built twice per release, roughly doubling build time.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

SHA256SUMS recorded dist/ prefixes so sha256sum -c failed next to the
downloaded binary. Emit basenames. Parallel PostgresPlane connects raced
HASH convert GRANT with SET ROLE GRANT (tuple concurrently updated);
hold the advisory lock across both. Do not re-implement HASH layout.
Issue #84 remainder. GitHub OIDC signs the binary, checksums, SBOMs, and
the GHCR image by digest. Release is created only after signatures.
Syft SPDX fails closed without syft or non-SPDX JSON. NIST SP 800-218
is attached. Do not re-implement checksums or store slices.

Copy link
Copy Markdown
Contributor Author

Rust CI postgres_expired_lease_is_reclaimed_and_skip_locked_is_exclusive failed on tuple concurrently updated during HASH convert GRANT racing SET ROLE GRANT. migrate() now holds the process gate and pg_advisory_lock across apply_schema and assume_runtime_role. Covered by postgres_parallel_connect_does_not_race_grants. Do not re-implement HASH layout.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

Comment thread src/control_plane.rs
Comment on lines 647 to 662
.execute("SELECT pg_advisory_lock($1)", &[&MIGRATION_LOCK_KEY])
.await
.map_err(|error| format!("control plane migration lock failed: {error}"))?;
let result = apply_schema(&client).await;
// HASH convert GRANT and SET ROLE GRANT both mutate pg_class ACL
// tuples. Hold the lock across both so parallel connects cannot
// `tuple concurrently updated`.
let result = async {
apply_schema(&client).await?;
assume_runtime_role(&client).await
}
.await;
let _ = client
.execute("SELECT pg_advisory_unlock($1)", &[&MIGRATION_LOCK_KEY])
.await;
result
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Runtime role GRANT moved under migration lock

assume_runtime_role now runs inside migrate() while holding both MIGRATION_GATE and the advisory lock, and the standalone method was removed (no other caller referenced it). The advisory unlock at control_plane.rs now runs after SET ROLE wardnet_runtime, but pg_advisory_unlock is public and locks are session-owned, so it still succeeds.

(Refers to this code)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread .github/workflows/release.yml Outdated
echo "$GH_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin
image="ghcr.io/contextualwisdomlab/waf-ids-ai-soc"
tag="${GITHUB_REF_NAME}"
docker build -t "${image}:${tag}" .

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Release docker build omits build.rs

The workflow's docker build uses a Dockerfile that copies only Cargo.toml, Cargo.lock, src, and crates, not build.rs. The stub script's only consumer, env!("WARDNET_CORAZA_ABI_STUB") at src/coraza_inprocess.rs:415, is #[cfg(test)], so the release build compiles without it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Issue #84 remainder. Annotated vX.Y.Z tags only; lightweight tags fail
closed before the release job builds. Kubernetes pin is the GHCR
content digest; tag aliases are refused. Do not re-implement checksums
or cosign/SBOM.
Issue #11 first slice. The build-script libcoraza ABI stub gains a
deterministic battery covering SQLi (942100), XSS (941100), path traversal
(930100), Unix RCE (932100, with first-match ordering so '; cat /etc/passwd'
attributes to RCE over traversal), and Log4j JNDI (944120) in raw and
percent-encoded forms across URI and POST-body phases.

tests/binary.rs now starts the real gateway with the stub engine, creates a
block route through the admin API, fires nine cases over HTTP, and asserts
each is 403-blocked citing the expected CRS rule id while a benign request
still forwards; /api/events must record one event per attempt with the
forwarded client IP kept unmasked.

Doctoring: docs/doctoring/ci-attack-evidence-battery.md grounds the split
between detection-path evidence (CI) and detection efficacy (operator-supplied
libcoraza + Core Rule Set), APA 7th.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head d810fc43ae987a0ceabc3051732a1411a72e3883.

  • Head SHA: d810fc43ae987a0ceabc3051732a1411a72e3883

  • Workflow run: 32702432576

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Workflow: release.yml"]
  S1 --> I1["GitHub Actions review job"]
  I1 --> R1["Review risk: Workflow: release.yml"]
  R1 --> V1["actionlint plus required checks"]
  Evidence --> S2["Changed file (5 files)"]
  S2 --> I2["repository behavior"]
  I2 --> R2["Review risk: Changed file (5 files)"]
  R2 --> V2["required checks"]
  Evidence --> S3["Docs (3 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> R3["Review risk: Docs (3 files)"]
  R3 --> V3["docs review"]
  Evidence --> S4["Test: binary.rs"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test: binary.rs"]
  R4 --> V4["targeted test run"]
Loading

@opencode-agent

Copy link
Copy Markdown
Contributor

OpenCode Review Overview

  • Head SHA: d810fc43ae987a0ceabc3051732a1411a72e3883
  • Workflow run: 32702432576
  • Workflow attempt: 1
  • Gate result: REQUEST_CHANGES (approval step)

Pull request overview

OpenCode cannot approve yet because required coverage evidence did not pass.

Review outcome

1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence

  • Problem: The required coverage-evidence job result was failure, so OpenCode cannot establish approval sufficiency for this head.

  • Root cause: Automated approval is only valid when the same-head coverage-evidence job proves supported repository test suites passed and configured docstring gates passed or were advisory, or reports not applicable because no supported source files or package manifests exist. Missing, failed, skipped, unavailable, or unsupported-tooling test evidence is a blocker.

  • Fix: Install or configure the repository test/docstring evidence tooling when source files or package manifests exist, rerun the current-head coverage-evidence job, and approve only after it reports success with required evidence or explicit no-source not-applicable evidence.

  • Regression test: Keep the approval branch checking needs.coverage-evidence.result == success before posting APPROVE, and publish REQUEST_CHANGES when coverage-evidence blocker states such as cancelled, skipped, failed, unsupported-tooling, or below-100 evidence are present.

  • Result: REQUEST_CHANGES

  • Reason: coverage-evidence result was failure, so required test/docstring evidence was not proven for current head d810fc43ae987a0ceabc3051732a1411a72e3883.

  • Head SHA: d810fc43ae987a0ceabc3051732a1411a72e3883

  • Workflow run: 32702432576

  • Workflow attempt: 1

Coverage evidence

Coverage Decision

  • Result: FAIL
  • Test evidence: not proven passing
  • Docstring evidence: not proven passing when configured
  • Failure count: 1

Changed-File Evidence Map

flowchart LR
  PR["PR changed files"] --> Evidence["OpenCode bounded evidence"]
  Evidence --> S1["Workflow: release.yml"]
  S1 --> I1["GitHub Actions review job"]
  I1 --> R1["Review risk: Workflow: release.yml"]
  R1 --> V1["actionlint plus required checks"]
  Evidence --> S2["Changed file (5 files)"]
  S2 --> I2["repository behavior"]
  I2 --> R2["Review risk: Changed file (5 files)"]
  R2 --> V2["required checks"]
  Evidence --> S3["Docs (3 files)"]
  S3 --> I3["operator or user guidance"]
  I3 --> R3["Review risk: Docs (3 files)"]
  R3 --> V3["docs review"]
  Evidence --> S4["Test: binary.rs"]
  S4 --> I4["regression suite"]
  I4 --> R4["Review risk: Test: binary.rs"]
  R4 --> V4["targeted test run"]
Loading

…-evidence-ci

feat(waf): CI attack-evidence battery against the live binary (issue #11)
…ed-tag-admission

feat(release): refuse lightweight tags and pin k8s by digest
…-sbom

feat(release): keyless cosign, SPDX SBOM, and SLSA on the same tag
Base automatically changed from feat/issue-81-outbox-consumers to feat/issue-80-optimistic-concurrency August 25, 2026 23:41

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Open in Devin Review

tag="${GITHUB_REF_NAME}"
docker build -t "${image}:${tag}" .
push_out="$(docker push "${image}:${tag}")"
digest="$(printf '%s\n' "$push_out" | awk '/digest:/{d=$NF} END{print d}')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Release digest parse grabs image size, not sha256

awk '/digest:/{d=$NF}' takes the last field of the docker push line <tag>: digest: sha256:<hex> size: <number>, which is the size number, not the digest. ref becomes ghcr.io/...@<size>, so the SBOM, cosign signing by digest, the provenance/SBOM attestations, and IMAGE-DIGEST.txt all get an invalid reference.

Suggested change
digest="$(printf '%s\n' "$push_out" | awk '/digest:/{d=$NF} END{print d}')"
digest="$(printf '%s\n' "$push_out" | grep -oE 'sha256:[0-9a-f]{64}' | tail -n1)"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src/coraza_abi_stub.rs
Comment on lines +112 to +170
// RCE and Log4j entries precede traversal entries because command
// fixtures such as `; cat /etc/passwd` also contain traversal-looking
// substrings; first-match ordering keeps rule attribution deterministic.
BatteryEntry {
needle: "; cat ",
rule_id: 932100,
message: RCE_MESSAGE,
},
BatteryEntry {
needle: "%3b%20cat%20",
rule_id: 932100,
message: RCE_MESSAGE,
},
BatteryEntry {
needle: "/bin/sh",
rule_id: 932100,
message: RCE_MESSAGE,
},
BatteryEntry {
needle: "$(whoami)",
rule_id: 932100,
message: RCE_MESSAGE,
},
BatteryEntry {
needle: "${jndi",
rule_id: 944120,
message: LOG4J_MESSAGE,
},
BatteryEntry {
needle: "%24%7bjndi",
rule_id: 944120,
message: LOG4J_MESSAGE,
},
BatteryEntry {
needle: "../",
rule_id: 930100,
message: TRAVERSAL_MESSAGE,
},
BatteryEntry {
needle: "..%2f",
rule_id: 930100,
message: TRAVERSAL_MESSAGE,
},
BatteryEntry {
needle: "..%252f",
rule_id: 930100,
message: TRAVERSAL_MESSAGE,
},
BatteryEntry {
needle: "etc/passwd",
rule_id: 930100,
message: TRAVERSAL_MESSAGE,
},
BatteryEntry {
needle: "etc%2fpasswd",
rule_id: 930100,
message: TRAVERSAL_MESSAGE,
},
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Coraza stub battery relies on needle ordering

RCE/Log4j needles are placed before traversal needles so overlapping payloads like ; cat /etc/passwd attribute to RCE (932100) rather than traversal (930100). Matching lowercases input and all needles are lowercase, so percent-encoded variants line up. This is a test-only fixture, not detection logic.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@seonghobae
seonghobae dismissed opencode-agent[bot]’s stale review August 26, 2026 02:28

Stale review: coverage-evidence now passes on this head; all required checks green.

@seonghobae
seonghobae merged commit 6a31ef0 into feat/issue-80-optimistic-concurrency Aug 26, 2026
7 checks passed
@seonghobae
seonghobae deleted the feat/issue-84-signed-release branch August 26, 2026 03:20
seonghobae added a commit that referenced this pull request Aug 26, 2026
* feat(security): fail-closed destination policy for outbound HTTP

One DestinationPolicy mediates gateway upstreams, threat-intel fetches,
Clearfolio, SOC LLM, and the Coraza sidecar URL. Private, loopback,
link-local, CGNAT, and metadata classes are denied unless
DESTINATION_ALLOWLIST (or loopback development) permits them;
DESTINATION_DENYLIST wins. Clients ignore ambient HTTP proxies and do
not follow redirects.

Refs #79.

* docs: record PR #96 in the product-technical gap baseline

* feat(security): harden destination policy per-IP CIDR and readiness order

CIDR allowlist matches apply per resolved address, authorize non-default
ports, and reject prefixes outside the address-family width. IPv6
site-local is a denied class. Hostnames that merely contain 0x are not
hex IP literals. AppState constructors default to production policy;
seeded fixtures opt into development. Blocking DNS runs on spawn_blocking
with a timeout. Persistence and destination-list validation complete
before the readiness line.

* feat(security): pin outbound HTTP to evaluated destination addresses

After destination policy allows a host, the reqwest client resolves
only those IPs so a rebinding answer cannot reach loopback, private,
or metadata classes. Host and SNI stay on the original name.
Unpinned hostnames fail closed instead of falling back to OS DNS.

* feat(waf): evaluate live gateway transactions with in-process libcoraza

Issue #86 remainder: dlopen operator-supplied libcoraza and drive the C ABI
on each /gateway request. Missing library or empty ruleset fail closed before
bind. CI stays hermetic with a fixture cdylib that exports the same symbols.

* docs: record PR #97 in the product-technical gap baseline

* feat(store): require PostgreSQL as the production control plane

Non-loopback binds fail closed without CONTROL_PLANE_DATABASE_URL.
Migrations create 3NF two-word tables with default-deny RLS. Snapshot
persist commits policy rows and audit records in one transaction.
Loopback still uses the JSON file or memory adapter.

* feat(store): transactional outbox and leased workers

Issue #81 first slice on the PostgreSQL control plane. Security events
append with an outbox row in one transaction instead of rewriting the
snapshot. Workers claim with SKIP LOCKED, retry, dead-letter, and
record unique receipts. Stdout SIEM is at-least-once; the receipt is
the exactly-once ack. Also deterministic ORDER BY on postgres loads
(still-valid #98 finding). Do not re-implement the postgres gate.

* feat(store): rustls for production PostgreSQL sslmode=require (#100)

* feat(store): rustls for production PostgreSQL sslmode=require

Issue #80 remainder. sslmode=require/verify-ca/verify-full connect with
rustls and Mozilla roots; certificates are always verified. allow/prefer
are still rejected so the process cannot silently drop to plaintext.
Live test against plaintext CI postgres proves fail-closed. Do not
re-implement the postgres gate or the outbox.

* docs: record PR #100 in the product-technical gap baseline

* fix(store): rewrite verify-full sslmode for tokio-postgres 0.7

Still-valid #100 Devin finding. tokio-postgres 0.7 only parses
disable/prefer/require. Map verify-ca/verify-full to require before
connect; rustls still verifies certificates. Password query-lookalikes
are left untouched.

* feat(store): bound outbox listing and prune processed rows (#101)

* feat(store): bound outbox listing and prune processed rows

Still-valid #99 finding. GET /api/outbox returns at most EVENT_LIMIT
rows (dead letters, then pending, then leased, then processed).
Processed outbox_message rows prune to that cap; receipts and dead
letters stay. Do not re-implement the outbox, postgres gate, or rustls.

* docs: record PR #101 in the product-technical gap baseline

* fix(store): prune processed outbox to EVENT_LIMIT on save and ack

Still-valid #101 Devin finding. Snapshot save and worker ack used
LIST_LIMIT (1000) while append used operator EVENT_LIMIT. Store the
configured cap on PostgresPlane so all three paths retain the same
processed-row bound. Receipts and dead letters stay.

* feat(store): logical backup and isolated restore drill (#102)

* feat(store): logical backup and isolated restore drill

Issue #80 remainder stacked on #101. GET /api/backup exports a hashed
tenant snapshot; POST /api/backup restores after schema and payload-hash
checks; POST /api/backup/drill restores into an isolated tenant, compares
unmasked invariants, and drops the drill rows. Declared RPO is last
successful export; declared RTO is 60s. File/memory adapters report
backup=disabled. Do not re-implement rustls, outbox, or retention.

* docs: record PR #102 in the product-technical gap baseline

* feat(store): non-owner PostgreSQL runtime role after migrate (#103)

* feat(store): non-owner PostgreSQL runtime role after migrate

Still-valid #98 finding. CI connects as a superuser, which bypasses
FORCE RLS. Migrations stay on the login role, then SET ROLE
wardnet_runtime (NOSUPERUSER, NOBYPASSRLS, not table owner). Missing
tenant GUC yields no rows; DROP TABLE and DISABLE RLS are denied.
Do not re-implement rustls, outbox, retention, or backup/restore.

* docs: record PR #103 in the product-technical gap baseline

* fix(store): restore logical backups across role-only schema versions

v3 only provisions wardnet_runtime and does not change table shape.
verify() accepts schema 2 through the current migration version so a
role-only upgrade cannot void the last pre-upgrade logical backup.

* feat(store): HASH-partition security_event by tenant (#104)

* feat(store): HASH-partition security_event by tenant

Convert unpartitioned security_event to PARTITION BY HASH (tenant_id)
with eight children under pg_advisory_lock. Rows keep unmasked client
IPs and paths. /healthz.event_partitions reports the child count.
Logical restore still accepts schema 2 through the current version.

* docs: record PR #104 in the product-technical gap baseline

* feat(store): optimistic concurrency on postgres snapshots

Issue #80 last remainder. tenant_account.snapshot_version must match
the loaded token or persist fails closed (HTTP 409). Restores overwrite.
File/memory stay single-writer. Do not re-implement rustls, outbox,
runtime role, HASH, or backup/restore.

* docs: record PR #105 in the product-technical gap baseline

* fix(store): keep postgres snapshot_version aligned after startup save

load_postgres was saving with OCC and leaving the in-memory token one
behind the database, so every later management write returned HTTP 409.
Advance the loaded snapshot_version to the value save() wrote.

* feat(store): outbox consumers for TAXII, Clearfolio, and orchestrator

Enqueue operator-triggered TAXII polls, Clearfolio submits, and
contextual-orchestrator SOC analysis on the PostgreSQL leased outbox.
Request path returns 202; GET /api/outbox/{id} exposes receipt evidence.
Secrets stay in the credential registry. Startup postgres save advances
snapshot_version so the first management write cannot false-conflict.

* docs: record PR #106 in the product-technical gap baseline

* feat(release): tagged GitHub Release with SHA-256 and immutable GHCR (#107)

* feat(release): tagged GitHub Release with SHA-256 and immutable GHCR

Issue #84 first slice. A vX.Y.Z tag builds a locked binary, checksums,
a GitHub Release, and ghcr.io/contextualwisdomlab/waf-ids-ai-soc:vX.Y.Z
with no moving latest tag. Promotion and rollback are tag-for-tag.
Do not re-implement store slices or OCC.

* docs: record PR #107 in the product-technical gap baseline

* fix(release): basename checksums and serialize postgres GRANTs

SHA256SUMS recorded dist/ prefixes so sha256sum -c failed next to the
downloaded binary. Emit basenames. Parallel PostgresPlane connects raced
HASH convert GRANT with SET ROLE GRANT (tuple concurrently updated);
hold the advisory lock across both. Do not re-implement HASH layout.

* feat(release): keyless cosign, SPDX SBOM, and SLSA on the same tag

Issue #84 remainder. GitHub OIDC signs the binary, checksums, SBOMs, and
the GHCR image by digest. Release is created only after signatures.
Syft SPDX fails closed without syft or non-SPDX JSON. NIST SP 800-218
is attached. Do not re-implement checksums or store slices.

* feat(release): refuse lightweight tags and pin k8s by digest

Issue #84 remainder. Annotated vX.Y.Z tags only; lightweight tags fail
closed before the release job builds. Kubernetes pin is the GHCR
content digest; tag aliases are refused. Do not re-implement checksums
or cosign/SBOM.

* docs: record PR #109 in the product-technical gap baseline

* feat(waf): detect OWASP CRS attack battery on the live binary

Issue #11 first slice. The build-script libcoraza ABI stub gains a
deterministic battery covering SQLi (942100), XSS (941100), path traversal
(930100), Unix RCE (932100, with first-match ordering so '; cat /etc/passwd'
attributes to RCE over traversal), and Log4j JNDI (944120) in raw and
percent-encoded forms across URI and POST-body phases.

tests/binary.rs now starts the real gateway with the stub engine, creates a
block route through the admin API, fires nine cases over HTTP, and asserts
each is 403-blocked citing the expected CRS rule id while a benign request
still forwards; /api/events must record one event per attempt with the
forwarded client IP kept unmasked.

Doctoring: docs/doctoring/ci-attack-evidence-battery.md grounds the split
between detection-path evidence (CI) and detection efficacy (operator-supplied
libcoraza + Core Rule Set), APA 7th.

* fix(control-plane): close OCC and credential race gaps

* fix(release): capture pushed image digest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant