feat(release): keyless cosign, SPDX SBOM, and SLSA on the same tag - #108
Conversation
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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
| digest="$(printf '%s\n' "$push_out" | awk '/digest:/{d=$NF} END{print d}')" | ||
| test -n "$digest" |
There was a problem hiding this comment.
🔴 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.
| 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" |
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.
There was a problem hiding this comment.
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
successwith required evidence or explicit no-source not-applicable evidence. -
Regression test: Keep the approval branch checking
needs.coverage-evidence.result == successbefore 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 headef89a7e6ca046fd617d8181bacb94835cd4e5937. -
Head SHA:
ef89a7e6ca046fd617d8181bacb94835cd4e5937 -
Workflow run: 32702437228
-
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 (4 files)"]
S2 --> I2["repository behavior"]
I2 --> R2["Review risk: Changed file (4 files)"]
R2 --> V2["required checks"]
Evidence --> S3["Docs (6 files)"]
S3 --> I3["operator or user guidance"]
I3 --> R3["Review risk: Docs (6 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"]
OpenCode Review Overview
Pull request overviewOpenCode cannot approve yet because required coverage evidence did not pass. Review outcome1. HIGH .github/workflows/opencode-review.yml:1 - Coverage evidence did not prove required test/docstring evidence
Coverage evidenceCoverage Decision
Changed-File Evidence Mapflowchart 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 (4 files)"]
S2 --> I2["repository behavior"]
I2 --> R2["Review risk: Changed file (4 files)"]
R2 --> V2["required checks"]
Evidence --> S3["Docs (6 files)"]
S3 --> I3["operator or user guidance"]
I3 --> R3["Review risk: Docs (6 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"]
|
…-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
| // 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, | ||
| }, | ||
| ]; |
There was a problem hiding this comment.
📝 Info: First-match ordering attributes overlapping payloads correctly
BATTERY lists command-injection and Log4j needles before traversal needles, so ; cat /etc/passwd matches ; cat (932100) rather than etc/passwd (930100). First-match ordering is intentional and covered by tests; verified consistent across the URI, header, and body match paths.
Was this helpful? React with 👍 or 👎 to provide feedback.
| kind="$(git cat-file -t "$ref" 2>/dev/null || true)" | ||
| if [[ "$kind" != "tag" ]]; then | ||
| echo "admit-release-tag: $ref is ${kind:-missing}, not an annotated tag; use git tag -a" >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
🔍 Tag-kind check depends on the tag object being fetched
git cat-file -t "$ref" distinguishes annotated (tag) from lightweight (commit), which requires the tag object present locally. The workflow sets fetch-depth: 0; confirm the checkout of a tag-triggered run actually materializes the annotated tag object so admission does not misclassify a valid annotated tag.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Issue #84 remainder. Does not re-implement checksums (#107), sidecar, pin, libcoraza, postgres gate, outbox, rustls, OCC, or consumers.
The same
vX.Y.Ztag now:scripts/release-sbom.sh(fail closed without Syft / non-SPDX JSON)SHA256SUMS, SBOMs, andIMAGE-DIGEST.txt(cosign sign-blob, GitHub OIDC, no stored Cosign key)Promotion authority is the digest in
IMAGE-DIGEST.txt, not the tag alias. No movinglatest. NIST SP 800-218 is committed underdocs/papers/.Stacked on #107 (
feat/issue-84-signed-release). Merge order: #95, then #96, then #97, then #98, then #99, then #105, then #106, then #107, then this PR. Org ruleset 18156473 still requires two independent approvals; do not--adminmerge.Tests
cargo fmt --checkcargo test --locked --workspace(includes basename checksums, SBOM fail-closed, workflow keyless contract, parallel postgres GRANT connect)cargo clippy --locked --workspace --all-targets -- -D warningsscripts/smoke.shruns (/healthz+/admin+/api/commercial/readinessat 2B KRW, evidence includesdocs/doctoring/signed-release.md)Remaining on #84: admission that rejects unsigned tags and an ephemeral production-shaped deploy of the signed digest.