-
Notifications
You must be signed in to change notification settings - Fork 0
draft(preservation): retain Coraza/CRS enforcement evidence for bounded successor #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
48
commits into
main
Choose a base branch
from
feat/issue-86-in-path-coraza
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
54604c2
feat(waf): consult Coraza sidecar on live gateway transactions
seonghobae ba9ee3a
docs: record PR #95 in the product-technical gap baseline
seonghobae 80daf7a
fix(waf): bound, attribute, and evidence the Coraza sidecar path
seonghobae 63b7930
fix(waf): treat any 403 sidecar answer as an interruption
seonghobae ee40769
Merge branch 'main' into feat/issue-86-in-path-coraza
seonghobae 26d2064
Merge branch 'main' into feat/issue-86-in-path-coraza
seonghobae ad3675a
feat(security): fail-closed destination policy for outbound HTTP (#96)
seonghobae 5860f42
fix(waf): honor explicit engine interruptions
seonghobae 4a8de40
fix(runtime): close sidecar and DNS fail-open gaps
seonghobae 4d8f7c1
fix(control-plane): preserve fresh tenant identity
seonghobae 9001f6f
fix(security): update Hickory and close protocol gaps
seonghobae 454f640
docs: refresh exact-head product gap baseline
seonghobae ae66601
fix: harden DNS and engine fail-closed paths
seonghobae f6eabf3
feat(store): optimistic concurrency on postgres snapshots (#105)
seonghobae 4f1ce3f
feat(mcp): expose stateless Wardnet status tool (#117)
seonghobae 33c692d
fix(control-plane): replace snapshot security events
seonghobae 723aed0
fix(release): scope publishing permissions to job
seonghobae 255de59
feat(dnsbl): serve authoritative DNS responses (#118)
seonghobae 52825e6
fix(dns): avoid duplicate approved-address cache write
seonghobae 9cdd2d7
fix(waf): prefer explicit sidecar interruptions
seonghobae 5901550
perf(gateway): add k6 load evidence and skip no-op clones
seonghobae ad1208a
docs(gaps): record measured coverage shortfall
seonghobae e69a72d
docs(gateway): ground tail-latency evidence
seonghobae 6aa8e23
Merge pull request #119 from ContextualWisdomLab/test/gateway-k6-e2e
seonghobae 53d5403
docs(gaps): refresh exact-head delivery evidence
seonghobae 8efdf53
Merge pull request #120 from ContextualWisdomLab/docs/refresh-gap-bas…
seonghobae 061da0c
test(gateway): distinguish connection churn load (#121)
seonghobae 8b97784
feat(dnsbl): resolve IPv6 nibble queries
seonghobae 97d69f8
Merge pull request #122 from ContextualWisdomLab/feat/dnsbl-ipv6-nibble
seonghobae cc83ded
fix(runtime): install SIGTERM handler before readiness
seonghobae 44263b6
docs: refresh live product gap evidence
seonghobae 3f8717f
fix(runtime): preserve mixed credential provenance
seonghobae 233c831
fix(control-plane): require TLS for public bind
seonghobae 5934c13
docs(operations): document mixed credential source
seonghobae 58a2217
feat(api): expose egress boundary diagnostics (#125)
seonghobae 129bb96
fix(release): tighten exact-head review contracts
seonghobae 9114d2c
fix(control-plane): harden exact-head PR95 gates
codex 9e79e44
fix(control-plane): version durable event appends
codex a95e116
test(control-plane): re-seal mutated logical backup
codex 7932dd6
fix(control-plane): fail closed on stalled effects
codex 3c5b3eb
fix(control-plane): reject duplicate sslmode values
codex d0e93ee
fix(control-plane): export backups from one snapshot
codex 902e46e
fix(control-plane): keep event RLS on partition parent
codex 413bdd0
fix(control-plane): set backup isolation before tenant scope
codex dc388a5
fix(control-plane): serialize restore and allocate event ids
codex 0e02594
fix(control-plane): serialize restore audit persistence
codex fcf200b
docs: tighten evidence claims and research grounding
codex 304f053
docs(doctoring): add research impact summaries
codex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| name: Release | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - "v*.*.*" | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| release: | ||
| permissions: | ||
| contents: write | ||
| packages: write | ||
| id-token: write | ||
| attestations: write | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Admit annotated vX.Y.Z tag only | ||
| run: scripts/admit-release-tag.sh "$GITHUB_REF_NAME" | ||
| - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable | ||
| with: | ||
| toolchain: stable | ||
| - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 | ||
| - uses: anchore/sbom-action/download-syft@a930d0ac434e3182448fe678398ba5713717112a # v0.21.0 | ||
| - name: Build release binary | ||
| run: cargo build --locked --release | ||
| - name: Stage binary and binary SBOM | ||
| run: | | ||
| mkdir -p dist | ||
| cp target/release/waf-ids-ai-soc dist/waf-ids-ai-soc-linux-x86_64 | ||
| scripts/release-sbom.sh --output dist/sbom.spdx.json dist/waf-ids-ai-soc-linux-x86_64 | ||
| - name: Publish GHCR image by digest | ||
| id: image | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| 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}" . | ||
| push_out="$(docker push "${image}:${tag}")" | ||
| digest="$(printf '%s\n' "$push_out" | awk '{for (i=1;i<=NF;i++) if ($i ~ /^sha256:/) d=$i} END{print d}')" | ||
| test -n "$digest" | ||
| ref="${image}@${digest}" | ||
| printf '%s\n' "$ref" > dist/IMAGE-DIGEST.txt | ||
| scripts/release-sbom.sh --output dist/image.sbom.spdx.json "$ref" | ||
| { | ||
| echo "ref=${ref}" | ||
| echo "digest=${digest}" | ||
| echo "image=${image}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| - name: Checksums and keyless blob signatures | ||
| run: | | ||
| set -euo pipefail | ||
| (cd dist && ../scripts/release-checksums.sh \ | ||
| waf-ids-ai-soc-linux-x86_64 \ | ||
| sbom.spdx.json \ | ||
| image.sbom.spdx.json \ | ||
| IMAGE-DIGEST.txt > SHA256SUMS) | ||
| cat dist/SHA256SUMS | ||
| cosign sign-blob --yes \ | ||
| --bundle dist/waf-ids-ai-soc-linux-x86_64.sigstore.json \ | ||
| dist/waf-ids-ai-soc-linux-x86_64 | ||
| cosign sign-blob --yes \ | ||
| --bundle dist/SHA256SUMS.sigstore.json \ | ||
| dist/SHA256SUMS | ||
| cosign sign-blob --yes \ | ||
| --bundle dist/sbom.spdx.json.sigstore.json \ | ||
| dist/sbom.spdx.json | ||
| cosign sign-blob --yes \ | ||
| --bundle dist/image.sbom.spdx.json.sigstore.json \ | ||
| dist/image.sbom.spdx.json | ||
| cosign sign-blob --yes \ | ||
| --bundle dist/IMAGE-DIGEST.txt.sigstore.json \ | ||
| dist/IMAGE-DIGEST.txt | ||
| - name: Sign image and attest image SBOM (keyless) | ||
| run: | | ||
| set -euo pipefail | ||
| ref="${{ steps.image.outputs.ref }}" | ||
| cosign sign --yes "$ref" | ||
| cosign attest --yes --predicate dist/image.sbom.spdx.json --type spdxjson "$ref" | ||
| - name: SLSA provenance for binary | ||
| uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 | ||
| with: | ||
| subject-path: dist/waf-ids-ai-soc-linux-x86_64 | ||
| - name: SLSA provenance for image | ||
| uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 | ||
| with: | ||
| subject-name: ghcr.io/contextualwisdomlab/waf-ids-ai-soc | ||
| subject-digest: ${{ steps.image.outputs.digest }} | ||
| push-to-registry: true | ||
| - name: Attest binary SBOM | ||
| uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0 | ||
| with: | ||
| subject-path: dist/waf-ids-ai-soc-linux-x86_64 | ||
| sbom-path: dist/sbom.spdx.json | ||
| - name: Attest image SBOM | ||
| uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0 | ||
| with: | ||
| subject-name: ghcr.io/contextualwisdomlab/waf-ids-ai-soc | ||
| subject-digest: ${{ steps.image.outputs.digest }} | ||
| sbom-path: dist/image.sbom.spdx.json | ||
| push-to-registry: true | ||
| - name: GitHub Release | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| set -euo pipefail | ||
| notes="$(mktemp)" | ||
| { | ||
| echo "Promotion authority is the image digest and Sigstore signatures, not the tag." | ||
| echo | ||
| echo "Image: \`${{ steps.image.outputs.ref }}\`" | ||
| echo "Tag alias: \`ghcr.io/contextualwisdomlab/waf-ids-ai-soc:${GITHUB_REF_NAME}\`" | ||
| echo | ||
| echo "Verify: \`docs/runbooks/release.md\`" | ||
| } > "$notes" | ||
| gh release create "$GITHUB_REF_NAME" \ | ||
| dist/waf-ids-ai-soc-linux-x86_64 \ | ||
| dist/SHA256SUMS \ | ||
| dist/sbom.spdx.json \ | ||
| dist/image.sbom.spdx.json \ | ||
| dist/IMAGE-DIGEST.txt \ | ||
| dist/waf-ids-ai-soc-linux-x86_64.sigstore.json \ | ||
| dist/SHA256SUMS.sigstore.json \ | ||
| dist/sbom.spdx.json.sigstore.json \ | ||
| dist/image.sbom.spdx.json.sigstore.json \ | ||
| dist/IMAGE-DIGEST.txt.sigstore.json \ | ||
| --notes-file "$notes" \ | ||
| --verify-tag |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Changelog | ||
|
|
||
| All notable changes to this project are documented in this file. | ||
|
|
||
| The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## Unreleased | ||
|
|
||
| ### Security | ||
|
|
||
| - Release admission refuses lightweight/unsigned `vX.Y.Z` tags (`scripts/admit-release-tag.sh`). Kubernetes pin is the GHCR content digest (`scripts/pin-k8s-digest.sh`); tag aliases are rejected. | ||
| - Tagged releases (`vX.Y.Z`) build a locked binary, basename SHA-256 checksums, SPDX SBOMs (binary and image), keyless Sigstore signatures (OIDC, no stored Cosign key), GitHub SLSA provenance, and a GHCR image signed **by digest**. The GitHub Release is created only after signatures succeed. Promotion authority is the digest in `IMAGE-DIGEST.txt`, not the tag alias. No moving `latest` tag (`docs/runbooks/release.md`). | ||
| - PostgreSQL outbox consumers for TAXII poll, Clearfolio document submit, and contextual-orchestrator SOC analysis (issue #81 remainder). Operator-triggered HTTP leaves through `taxii.collection_polled`, `clearfolio.document_submitted`, and `soc.analysis_requested` with leased-worker retries and unique receipts. Request path returns HTTP 202 and `GET /api/outbox/{message_id}` exposes receipt evidence. Secrets never enter outbox payloads (TAXII bearer lives in the credential registry). File/memory adapters keep the previous synchronous path. Client IPs, paths, indicator values, and actor names stay unmasked. LLM analysis remains advisory and never auto-enforces. | ||
|
|
||
|
|
||
| - PostgreSQL `security_event` is HASH-partitioned by `tenant_id` (8 children). Unpartitioned tables convert in place and keep unmasked client IPs and paths. `/healthz.event_partitions` reports the child count (0 on file/memory). Logical restore still accepts schema 2 through the current migration version; HASH does not change the snapshot shape. | ||
| - PostgreSQL snapshot persist is optimistic-concurrency: `tenant_account.snapshot_version` must match the loaded token or the write returns a snapshot conflict (HTTP 409). Restores overwrite. File/memory adapters stay single-writer. | ||
| - PostgreSQL control-plane runtime is `wardnet_runtime` (NOSUPERUSER, NOBYPASSRLS, not the table owner). Migrations run as the login role, then `SET ROLE` so FORCE RLS binds even when the URL user is a superuser. Missing `wardnet.tenant_id` yields no rows. DDL (`DROP TABLE`, `DISABLE ROW LEVEL SECURITY`) is denied. Logical restore accepts schema 2 through the current migration version so a role-only upgrade cannot void the last pre-upgrade backup. | ||
| - PostgreSQL control-plane logical backup and isolated restore drill (issue #80 remainder). `GET /api/backup` exports a hashed tenant snapshot (policy, events, outbox, receipts). `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 tenant. Declared RPO is the last successful export; declared RTO is 60 seconds. File/memory adapters report `/healthz.backup=disabled`. Client IPs, paths, and actor names stay unmasked. | ||
| - PostgreSQL control-plane mutations enqueue a transactional outbox row in the same transaction (issue #81). Security events append incrementally instead of rewriting the snapshot. A leased worker claims with `FOR UPDATE SKIP LOCKED`, retries with bounded backoff, dead-letters exhausted/permanent failures, and records unique receipts. Stdout SIEM export is at-least-once; the receipt is the exactly-once ack. `/healthz.outbox` and `GET /api/outbox` are operator-visible; `POST /api/outbox/{id}/replay` requeues dead letters with audit. File/memory adapters report `outbox=disabled`. `GET /api/outbox` is bounded to `EVENT_LIMIT` (dead letters and pending first). Processed `outbox_message` rows are pruned to that same cap on append, snapshot save, and worker ack; receipts stay as the exactly-once ack. | ||
| - Control-plane PostgreSQL URLs honor `sslmode=require` / `verify-ca` / `verify-full` with rustls and Mozilla roots (certificates always verified). tokio-postgres 0.7 only parses `require`, so verification modes are rewritten to `require` before connect. `sslmode=allow` / `prefer` are rejected so the process cannot silently drop to plaintext. | ||
| - Production (non-loopback) binds fail closed without `CONTROL_PLANE_DATABASE_URL`. PostgreSQL is the production control-plane authority (3NF two-word tables, default-deny row-level security, snapshot persist in one transaction). Loopback still uses the JSON file / memory adapter. `/healthz.persistence` reports `postgres`, `file`, or `memory`. The URL is a secret and is bootstrapped into the credential registry. | ||
| - Live `/gateway` transactions consult in-process libcoraza when `CORAZA_LIB_PATH` is set (with `CORAZA_RULES_PATH` and/or `CORAZA_DIRECTIVES`). Missing library, missing rules, or an empty ruleset fail startup before bind. Otherwise a Coraza sidecar is consulted when `CORAZA_WAF_URL` is set. The sidecar response is parsed with the existing Coraza audit adapter (OWASP CRS authority, not a hand-rolled engine). Engine outage is fail-closed when `PROVEN_ENGINE_FAIL_CLOSED` is true. `GET /api/waf/engine-status` and `/healthz.proven_engine` report `coraza_in_process`, `coraza_sidecar`, or `ingest_hints_only`. | ||
| - Fail-closed destination policy on every outbound `http`/`https` call, including the Coraza sidecar URL (gateway upstream, threat-intel fetch, Clearfolio, SOC LLM). Private/loopback/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. CIDR allowlist matches apply per resolved address; CIDR entries authorize non-default ports; IPv6 site-local (`fec0::/10`) is denied; invalid CIDR prefixes fail startup; `/healthz.destination_mode` reports the policy class. Blocking DNS runs on `spawn_blocking` with a 2s timeout. Persistence and destination-list validation complete before the readiness line is printed. After a host is allowed, the HTTP client connects only to those evaluated addresses (Host/SNI unchanged) so a rebinding answer cannot bypass the policy. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.