Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ jobs:
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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### 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.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ flowchart LR
- `crates/waf-ids-core`: reusable domain models plus validation, upsert, scoring, DNSBL zone export, event retention, threat-feed freshness, KPI snapshot, and commercial readiness logic.
- `/admin`: embedded web console.
- `/gateway/{path}`: route selection, request scoring, monitor/block decision, optional upstream proxying.
- `.github/workflows/release.yml`: tag `vX.Y.Z` builds a locked binary, basename SHA-256 checksums, SPDX SBOMs, keyless Sigstore signatures, SLSA provenance, and a GHCR image signed by digest. The GitHub Release is created only after signatures succeed. Rollback is the previous digest (`docs/runbooks/release.md`).
- `.github/workflows/release.yml`: annotated `vX.Y.Z` tags only (lightweight tags are refused). Builds a locked binary, basename SHA-256 checksums, SPDX SBOMs, keyless Sigstore signatures, SLSA provenance, and a GHCR image signed by digest. Kubernetes pin is `IMAGE-DIGEST.txt` (`docs/runbooks/release.md`).
- `/dnsbl/zone`: DNSBL zone text using the configured origin, suitable for publication through an authoritative DNS server.
- `/api/commercial/license`: tenant/license metadata for commercial packaging.
- `/api/commercial/readiness`: computed 2B KRW sale-readiness checks and blockers.
Expand Down
71 changes: 71 additions & 0 deletions docs/doctoring/ci-attack-evidence-battery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Doctoring — CI attack-evidence battery (issue #11)

This note grounds the issue #11 slice: the compiled gateway binary is started
in CI with a hermetic libcoraza engine, a deterministic OWASP CRS attack
battery is fired over real HTTP, and every attempt must be blocked with the
cited CRS rule id and recorded as a security event that keeps the forwarded
client IP unmasked.

## What is proven (and what is not)

Proven end to end on the real binary: operator-supplied `CORAZA_LIB_PATH`
loading, rules-file admission, per-transaction evaluation of method/URI/body,
block responses citing `coraza/crs: rule <id>`, benign traffic still
forwarding, and unmasked client attribution in `/api/events`.

Not proven: detection *quality* against arbitrary live traffic. The CI engine
is the build-script ABI stub (`src/coraza_abi_stub.rs`), a fixture that
mirrors the libcoraza C ABI, not Coraza itself. Quality evidence stays with an
operator deployment using a real libcoraza plus the OWASP Core Rule Set; this
slice only removes "the path was never exercised in CI" from the gap list.

## Adopted standards and literature

OWASP Foundation. (n.d.). *OWASP Core Rule Set documentation*.
https://coreruleset.org/docs/

- **Design impact:** Battery entries map to canonical CRS rule families —
942100 SQLi (libinjection), 941100 XSS (libinjection), 930100 path
traversal, 932100 Unix command injection, 944120 Log4j JNDI. Rule ids in
block reasons and events stay CRS ids so operator dashboards read the same
vocabulary in CI evidence and production.

Scarfone, K., & Mell, P. (2007). *Guide to intrusion detection and prevention
systems (IDPS)* (NIST Special Publication 800-94). National Institute of
Standards and Technology. https://doi.org/NIST.SP.800-94

- **Design impact:** IDPS evaluation distinguishes the detection *path* from
detection *efficacy*. SP 800-94's testing guidance motivates keeping the two
claims separate: CI asserts the prevention path (signature → interrupt →
block → record), while efficacy against evasive payloads requires curated
corpora and is explicitly out of scope for this fixture.

Saltzer, J. H., & Schroeder, M. D. (1975). The protection of information in
computer systems. *Proceedings of the IEEE*, *63*(9), 1278–1308.
https://doi.org/10.1109/PROC.1975.9939

- **Design impact:** Complete mediation and fail-safe defaults. The battery
runs through the same route pipeline (`mode: block`) as production traffic,
so no test-only bypass exists; an engine that fails to load refuses startup
before bind instead of degrading silently.

MITRE. (n.d.). *CWE-20: Improper input validation*. MITRE Corporation.
https://cwe.mitre.org/data/definitions/20.html

- **Design impact:** The battery covers encoded variants (`%3Cscript`,
`%24%7BJNDI`, `..%2F`) because input-validation defects classically live at
decoding boundaries; the gateway evaluates the raw request line exactly as
received, so fixtures pin that behavior rather than a decoded copy.

## Verification posture

- `tests/binary.rs::live_gateway_detects_owasp_attack_battery_end_to_end`
spawns the binary, creates the block route over the admin API, fires nine
battery cases (GET query attacks across five rule families plus a POST-body
XSS), asserts HTTP 403 + `engine=coraza` + cited rule id per case, asserts a
benign request forwards, and asserts `/api/events` records one event per
attempt with `X-Forwarded-For` preserved verbatim.
- `src/coraza_inprocess.rs::stub_engine_battery_matches_each_owasp_family`
pins the fixture contract itself, including first-match ordering so the
overlapping `; cat /etc/passwd` payload attributes to RCE (932100), not
traversal.
42 changes: 25 additions & 17 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Product and technical gap baseline

Snapshot date: 2026-08-24T05:55Z (exact-head inventory of then-open GitHub PRs
Snapshot date: 2026-08-24T06:10Z (exact-head inventory of then-open GitHub PRs
and Issues plus operator-perceptible gaps). Update this file on every hourly loop.

Commercial contract and `/api/commercial/readiness` remain **2B KRW**. The
Expand All @@ -25,6 +25,8 @@ not “waiting on review/CI time”.

| PR | Title | Head | Checks | Reviews | Merge blocker |
| --- | --- | --- | --- | --- | --- |
| [#110](https://github.com/ContextualWisdomLab/wardnet/pull/110) | feat(waf): CI attack-evidence battery against the live binary (issue #11) | `feat/issue-11-attack-evidence-ci` stacked on #109 | local fmt/test/clippy green; battery + fixture tests pass | Author this pass | Org 2-approval + self-author. Merge the stack below first; retarget to `main` when #109 lands. |
| [#109](https://github.com/ContextualWisdomLab/wardnet/pull/109) | feat(release): refuse lightweight tags and pin k8s by digest | `feat/issue-84-unsigned-tag-admission` stacked on #108 | local fmt/test/clippy + two `/healthz` smokes (2B KRW) | Author this pass | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #105 then #106 then #107 then #108 first. Do not `--admin`. |
| [#108](https://github.com/ContextualWisdomLab/wardnet/pull/108) | feat(release): keyless cosign, SPDX SBOM, SLSA on the same tag | `feat/issue-84-cosign-sbom` stacked on #107 | local fmt/test/clippy + two `/healthz` smokes (2B KRW, evidence includes signed-release doctoring) | Author this pass | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #105 then #106 then #107 first. Do not `--admin`. Do not re-implement checksums. |
| [#107](https://github.com/ContextualWisdomLab/wardnet/pull/107) | feat(release): tagged GitHub Release with SHA-256 and immutable GHCR | `feat/issue-84-signed-release` stacked on #106 | still-valid Devin basename checksums + rust GRANT race (`tuple concurrently updated`) fixed this pass; local fmt/test/clippy + two `/healthz` smokes | Author this pass; Devin COMMENTED (checksum thread still-valid, now fixed) | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #105 then #106 first. Do not `--admin`. |
| [#106](https://github.com/ContextualWisdomLab/wardnet/pull/106) | feat(store): outbox consumers for TAXII, Clearfolio, and orchestrator | `feat/issue-81-outbox-consumers` stacked on #105 | local fmt/test/clippy + smoke.sh + two `/healthz` and `/admin`/`/api/commercial/readiness` (2B KRW) | Author this pass | Org 2-approval + self-author. Merge #95 then #96 then #97 then #98 then #99 then #105 first. Do not `--admin`. Do not re-implement OCC or prior store slices. |
Expand Down Expand Up @@ -60,7 +62,7 @@ by ruleset `18156473` (not by failing Checks). Do not `--admin` merge.
| [#87](https://github.com/ContextualWisdomLab/wardnet/issues/87) | [Production readiness] Close the evidence-backed Wardnet production gate | medium |
| [#86](https://github.com/ContextualWisdomLab/wardnet/issues/86) | [P0] Put proven WAF/IDS engines in the enforcement path and publish detection-quality evidence | **critical — in-process + sidecar slices shipped, unmerged** |
| [#85](https://github.com/ContextualWisdomLab/wardnet/issues/85) | [P1] Establish production telemetry, SLOs, incident response, and disaster-recovery evidence | high |
| [#84](https://github.com/ContextualWisdomLab/wardnet/issues/84) | [P1] Build an immutable signed release, promotion, and rollback pipeline | **high — checksums/GHCR on #107; keyless cosign/SBOM/SLSA this pass; remaining admission/ephemeral deploy** |
| [#84](https://github.com/ContextualWisdomLab/wardnet/issues/84) | [P1] Build an immutable signed release, promotion, and rollback pipeline | **high — checksums/GHCR #107; cosign/SBOM #108; annotated-tag admission this pass** |
| [#83](https://github.com/ContextualWisdomLab/wardnet/issues/83) | [P1] Add bounded distributed admission control, trusted client attribution, and overload behavior | high |
| [#82](https://github.com/ContextualWisdomLab/wardnet/issues/82) | [P1] Integrate Keyverse identity, tenant authorization, consent, and human approval evidence | high (blocked) |
| [#81](https://github.com/ContextualWisdomLab/wardnet/issues/81) | [P0] Add a transactional outbox and idempotent leased workers for external effects | **critical — first slice on #99; retention on #101; TAXII/Clearfolio/orchestrator consumers this pass** |
Expand Down Expand Up @@ -202,22 +204,28 @@ holes on untouched handlers stay listed for later loops.

## This loop’s shipped gap

Issue **#84** remainder: the same `vX.Y.Z` tag keyless-signs the binary,
`SHA256SUMS`, SPDX SBOMs, and the GHCR image **by digest** (GitHub OIDC,
no stored Cosign key). SLSA provenance and SBOM attestations attach before
the GitHub Release is created. Promotion authority is `IMAGE-DIGEST.txt`,
not the tag alias. #107 still-valid findings this pass: basename checksums
so `sha256sum -c` works, and HASH/SET ROLE GRANTs serialized under the
advisory lock (`tuple concurrently updated` on parallel connect). Do not
re-implement #78–#81 store slices, OCC, or the checksum first slice.
Issue **#11** first slice (PR #110, stacked on #109): the build-script libcoraza
ABI stub now carries a deterministic OWASP CRS battery (942100 SQLi, 941100
XSS, 930100 traversal, 932100 RCE incl. `; cat /etc/passwd` overlap ordering,
944120 Log4j JNDI; raw + percent-encoded needles), and a live-binary test fires
nine cases over real HTTP asserting 403 + `engine=coraza` + cited rule id,
benign forwarding, and unmasked `X-Forwarded-For` attribution in `/api/events`.
Doctoring: `docs/doctoring/ci-attack-evidence-battery.md` (APA 7th). Detection
*quality* stays with operator-supplied real libcoraza + CRS; CI proves the path.

Strix infra-failure loop: PRs #72/#77/#93/#94/#95 failed strix on provider
infrastructure (`openai-direct/gpt-5.6-luna` exit-1 fallbacks and 5400 s NIM
timeouts — zero findings). All five were re-scanned via central
`repository_dispatch` (`strix-scan`) with matching base/head payloads. Do not
treat these as code findings; do not rotate review-agent keys.

## Next hourly loop (do, do not report)

1. Second independent APPROVE on #91/#92. `gh pr merge` rejected this pass
by ruleset 18156473. Do not `--admin`. Do not rotate `COPILOT_GITHUB_TOKEN`.
2. Keep #94 independently; #95 then #96 then #97 then #98 then #99 then #105
then #106 then #107 then this cosign PR merge-ready. Do not `--admin`.
3. Next runtime gap if policy still blocks: admission that rejects unsigned
tags / ephemeral production-shaped deploy of the signed digest (#84 rest),
or Keyverse identity (#82).
1. Merge stack bottom-up as agent approvals land: #95 then retarget+update
#96 → merge, then #97, #98, #99, #105, #106, #107, #108, #109, then
retarget #110 to `main`.
2. Second independent APPROVE comes from the OpenCode review agent via the
org scheduler (`Required PR Review Merge Scheduler`, budget 1/run); keep
heads current so dispatches bind to the exact head.
3. Resolve any new CHANGES_REQUESTED from opencode/noema on current heads.
4. Refresh this file’s PR/Issue tables from `gh pr list` / `gh issue list`.
16 changes: 11 additions & 5 deletions docs/runbooks/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ is committed at `docs/papers/nist-sp-800-218-ssdf.pdf`.

## Immutable artifacts

A git tag `vX.Y.Z` starts `.github/workflows/release.yml`, which:
A git tag `vX.Y.Z` starts `.github/workflows/release.yml`. Lightweight
tags are refused (`scripts/admit-release-tag.sh` requires an annotated
tag object). The workflow then:

1. Builds `waf-ids-ai-soc` with `cargo build --locked --release`
2. Writes basename `SHA256SUMS` via `scripts/release-checksums.sh`
Expand Down Expand Up @@ -59,9 +61,14 @@ changed.
## Promotion

1. Tag from the merge commit on `main`: `git tag -a vX.Y.Z -m "wardnet vX.Y.Z"`
(lightweight `git tag vX.Y.Z` is not admitted)
2. `git push origin vX.Y.Z`
3. Wait for the Release workflow
4. Point Kubernetes at the digest (not `latest`, not the tag alone):
4. Pin Kubernetes from `IMAGE-DIGEST.txt` (not `latest`, not the tag alone):

```bash
scripts/pin-k8s-digest.sh IMAGE-DIGEST.txt
```

```yaml
image: ghcr.io/contextualwisdomlab/waf-ids-ai-soc@sha256:<digest>
Expand All @@ -78,6 +85,5 @@ until a tagged image exists; bump that pin in the same release PR as the tag.
3. Confirm `/healthz` and `/api/commercial/readiness` on the rolled-back replica
4. Do not retag or overwrite an existing `v*` image

Declared rollback unit: one immutable digest. Remaining on #84: ephemeral
production-shaped deploy of the signed digest, admission that rejects
unsigned tags, and the coverage/attack evidence bundle.
Declared rollback unit: one immutable digest. Remaining on #84: coverage
and attack-evidence bundle for the signed artifacts.
19 changes: 19 additions & 0 deletions scripts/admit-release-tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Fail closed unless REF is an annotated git tag (lightweight/unsigned refs
# are not admitted to the release pipeline).
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <tag>" >&2
exit 1
fi
ref="$1"
if [[ ! "$ref" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "admit-release-tag: $ref is not a vX.Y.Z tag" >&2
exit 1
fi
kind="$(git cat-file -t "$ref" 2>/dev/null || true)"
Comment thread
seonghobae marked this conversation as resolved.
if [[ "$kind" != "tag" ]]; then
echo "admit-release-tag: $ref is ${kind:-missing}, not an annotated tag; use git tag -a" >&2
exit 1
fi
Comment on lines +14 to +18

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Admission checks annotated, not signed

The comment (admit-release-tag.sh) and CHANGELOG claim unsigned tags are refused, but the check only rejects lightweight tags via git cat-file -t. An unsigned annotated tag (git tag -a without -s) is admitted. Either add git tag -v verification or drop the 'unsigned' wording.

Open in Devin Review

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

echo "admit-release-tag: admitted annotated tag $ref"
19 changes: 19 additions & 0 deletions scripts/pin-k8s-digest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Fail closed unless IMAGE-DIGEST.txt is a GHCR content digest, then print
# the Kubernetes image line operators must pin (never a floating tag).
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: $0 <IMAGE-DIGEST.txt>" >&2
exit 1
fi
file="$1"
if [[ ! -f "$file" ]]; then
echo "pin-k8s-digest: missing $file" >&2
exit 1
fi
ref="$(tr -d '[:space:]' < "$file")"
if [[ ! "$ref" =~ ^ghcr\.io/contextualwisdomlab/waf-ids-ai-soc@sha256:[0-9a-f]{64}$ ]]; then
echo "pin-k8s-digest: refused non-digest or wrong image: $ref" >&2
exit 1
fi
printf 'image: %s\nimagePullPolicy: IfNotPresent\n' "$ref"
Loading
Loading