Capturing SBOM and SARIF as signed evidence
This tutorial wires two of the highest-value security attestors (sbom and sarif) into your CI pipeline. The goal isn't just to generate SBOMs and security findings, it's to make their existence provable, so a release-gate policy can enforce "this artifact must have a signed SBOM and SARIF attached, or it doesn't ship."
The patterns below are taken from Cole's reference implementation at github.com/testifysec/dropbox-clone.
What each attestor does
| Attestor | What it captures | When to enable it |
|---|---|---|
sbom | Parses any CycloneDX or SPDX JSON file in the captured products and embeds the document into the attestation. | Steps that produce an SBOM file, typically the build step, after running syft, trivy sbom, or another generator. |
sarif | Parses any SARIF result file in the captured products. | Steps that run a SAST scanner, gosec, CodeQL, Semgrep, Trivy fs scan, etc. |
Both are post-product attestors, they run after the wrapped command finishes and inspect the products it produced. So the trick is making sure the SBOM/SARIF file lands in the products glob.
Pattern 1: SBOM from a Go build with syft
Use two CI/lock steps — build then SBOM — so each tool's argv lands in its own command-run/v0.1 attestation, and the SBOM step's material/v0.3 Merkle root captures the build artifact as input to the SBOM:
- name: build
env:
CGO_ENABLED: "0"
with:
step: build
command: go build -o bin/myapp ./cmd/myapp
attestations: environment git github
cilock-args: --attestor-product-include-glob "bin/*"
- name: sbom
with:
step: sbom
command: syft bin/myapp -o cyclonedx-json=bin/bom.cdx.json
attestations: environment git github sbom
cilock-args: --attestor-product-include-glob "bin/*"
What happens:
- build step: material attestor records source-file digests.
go buildruns as CI/lock's direct child — its argv is recorded bycommand-run/v0.1.bin/myapplands inproduct/v0.3as a Merkle leaf. - sbom step: material attestor digests the working tree after build, so
bin/myappis captured as the SBOM step's input.syftruns as CI/lock's direct child. The CycloneDX SBOM lands inproduct/v0.3; thesbomattestor parses it and emits ahttps://cyclonedx.org/bompredicate. - A release-gate Rego policy can now verify that the SBOM was generated against the exact binary the build step produced — the SBOM step's
material/v0.3.merkleRootmust contain the same digest as the build step'sproduct/v0.3.merkleRootforbin/myapp. See verify-in-a-release-gate for the worked policy.
Don't chain go build && syft inside a single bash -c — that collapses two tools into one command-run attestation, drops the build's product-vs-SBOM-material cross-step link, and breaks the supply-chain BackRef graph CI/lock is meant to produce.
Pattern 2: SARIF from a SAST scanner
Same shape, different attestor. The trick is letting the SAST tool fail without failing the CI/lock step itself (you want the SARIF report regardless):
- name: sast
with:
step: sast
command: gosec -no-fail -fmt=sarif -out=gosec-results.sarif ./...
attestations: environment git github sarif
cilock-args: --attestor-product-include-glob "*.sarif"
The -no-fail flag tells gosec to return 0 even when it finds issues — without it, CI/lock's command-run/v0.1 attestor records a failed step and downstream attestors skip. The SARIF still carries the findings; the policy gate is the Rego over the captured SARIF, not the tool's exit code. (See tools/gosec for the full per-tool walkthrough.)
Adapting for other SAST tools — each has a comparable "don't fail on findings" flag so CI/lock's argv stays clean:
| Tool | Command (no shell wrapper) |
|---|---|
| gosec | gosec -no-fail -fmt=sarif -out=results.sarif ./... |
| Semgrep | semgrep --config p/security-audit --sarif --output=results.sarif . |
| CodeQL | Run github/codeql-action/analyze outside CI/lock first (it writes results.sarif); then a separate cilock run --step codeql -- sh -c 'cat results.sarif > codeql.sarif' captures the SARIF as a product. Yes the sh -c is a workaround — file an action FR upstream for native CI/lock support. |
| Trivy fs | trivy fs --format sarif --output results.sarif . |
| Checkov | checkov -d . -s -o sarif --output-file-path . (writes results_sarif.sarif; -s is the soft-fail flag) |