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
110 changes: 98 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,124 @@ permissions:
contents: write
packages: write
id-token: write
attestations: write

jobs:
release:
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 checksums
- 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
(cd dist && ../scripts/release-checksums.sh waf-ids-ai-soc-linux-x86_64 > SHA256SUMS)
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 '/digest:/{d=$NF} END{print d}')"
test -n "$digest"
Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Docker push digest parses the image size, not the digest

awk '/digest:/{d=$NF}' takes the last field of the docker push line <tag>: digest: sha256:<hex> size: <n>, which is the numeric size, not the sha256: digest. test -n passes on the non-empty number, so the release proceeds with ref=ghcr.io/...@<size> and feeds that bad digest to every cosign and attestation step.

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

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

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}" >> "$GITHUB_OUTPUT"
echo "digest=${digest}" >> "$GITHUB_OUTPUT"
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 \
--generate-notes \
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
- 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}"
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

- Tagged releases (`vX.Y.Z`) build a locked binary, SHA-256 checksums, a GitHub Release, and an immutable GHCR image (`ghcr.io/contextualwisdomlab/waf-ids-ai-soc:vX.Y.Z`). Promotion and rollback are tag-for-tag (`docs/runbooks/release.md`). No moving `latest` tag.
- 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
4 changes: 4 additions & 0 deletions crates/waf-ids-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,8 @@ pub fn commercial_readiness_snapshot_at(data: &AppData, now_unix: u64) -> Commer
"docs/security/threat-model.md".to_string(),
"docs/security/compliance-mapping.md".to_string(),
"docs/runbooks/release.md".to_string(),
"docs/doctoring/signed-release.md".to_string(),
"docs/papers/nist-sp-800-218-ssdf.pdf".to_string(),
],
}
}
Expand Down Expand Up @@ -1172,6 +1174,8 @@ pub fn buyer_evidence_manifest_at(data: &AppData, now_unix: u64) -> BuyerEvidenc
"docs/figma/enterprise-product-architecture.md".to_string(),
"docs/ponytail/2026-07-02-complexity-audit.md".to_string(),
"docs/runbooks/release.md".to_string(),
"docs/doctoring/signed-release.md".to_string(),
"docs/papers/nist-sp-800-218-ssdf.pdf".to_string(),
],
deployment_assets: readiness.deployment_assets,
}
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, SHA-256 checksums, a GitHub Release, and an immutable GHCR image. Rollback is the previous tag (`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.
46 changes: 46 additions & 0 deletions docs/doctoring/signed-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Doctoring — signed release, SBOM, and provenance

This note grounds issue #84 remainder (keyless Sigstore signatures and
SBOM/SLSA attestations on the same `vX.Y.Z` tag as checksums/GHCR).
IEEE/ACM PDFs are not redistributed. NIST SP 800-218 is a U.S. government
work and is committed at `docs/papers/nist-sp-800-218-ssdf.pdf`.

## Adopted standards and literature

Sigstore. (n.d.). *Cosign documentation*. https://docs.sigstore.dev/cosign/

- **Design impact:** The release workflow uses GitHub OIDC (`id-token: write`)
for keyless signing. No long-lived Cosign key is stored. Blobs (binary,
`SHA256SUMS`, SBOMs, image digest file) get `cosign sign-blob` bundles.
The GHCR image is signed by digest (`image@sha256:…`), never by a moving
tag.

SLSA Project. (2025). *SLSA specification version 1.2*.
https://slsa.dev/spec/v1.2/

- **Design impact:** `actions/attest-build-provenance` binds the binary and
the image digest to in-toto SLSA provenance. `actions/attest-sbom` binds
SPDX SBOMs to the same subjects. GitHub Release is created only after
signatures and attestations succeed.

National Institute of Standards and Technology. (2022). *Secure Software
Development Framework (SSDF) version 1.1* (NIST SP 800-218).
https://doi.org/10.6028/NIST.SP.800-218
`docs/papers/nist-sp-800-218-ssdf.pdf`

- **Design impact:** PS.3 / PW.4 — produce integrity evidence (checksums,
SBOM, signatures, provenance) for the shipped artifact. A tag alias is
not promotion authority; operators verify the digest and signatures
(`docs/runbooks/release.md`).

Anchore. (n.d.). *Syft*. https://github.com/anchore/syft

- **Design impact:** `scripts/release-sbom.sh` fails closed without Syft and
rejects non-SPDX JSON. Binary and container filesystem SBOMs are both
attached to the GitHub Release.

## Operator next action

Tag `vX.Y.Z` from the reviewed merge commit on `main`. After the Release
workflow finishes, verify with the commands in `docs/runbooks/release.md`.
Point Kubernetes at the digest in `IMAGE-DIGEST.txt`, not at `latest`.
Binary file added docs/papers/nist-sp-800-218-ssdf.pdf
Binary file not shown.
Loading
Loading