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โ
Generate the SBOM in the same cilock run step as the build itself, so cilock observes both the build artifact and its SBOM as a single unit:
- name: build + sbom
uses: aflock-ai/cilock-action@v1.0.1
with:
step: build
command: |
bash -c "
CGO_ENABLED=0 go build -o bin/myapp ./cmd/myapp &&
syft bin/myapp -o cyclonedx-json=bin/bom.cdx.json
"
attestations: environment git github sbom
cilock-args: --attestor-product-include-glob "bin/*"
What happens:
- Material attestor records digests of source files before the command runs.
- The shell runs
go buildthensyft, both write tobin/. - Product attestor digests every file in
bin/(the--attestor-product-include-glob "bin/*"filter). - SBOM attestor scans the products, finds
bom.cdx.json, parses it, embeds the SBOM into the attestation. - The whole thing gets signed in one DSSE envelope.
The result: one signed attestation containing the binary's digest and the SBOM that describes it.
Pattern 2: SARIF from a SAST scannerโ
Same shape, different attestor. The trick is letting the SAST tool fail without failing the cilock step itself (you want the SARIF report regardless):
- name: sast
uses: aflock-ai/cilock-action@v1.0.1
with:
step: sast
command: bash -c "gosec -fmt=sarif -out=gosec-results.sarif ./... || true"
attestations: environment git github sarif
cilock-args: --attestor-product-include-glob "*.sarif"
The || true lets gosec exit non-zero when it finds issues without failing the cilock run. The SARIF attestor parses gosec-results.sarif and embeds the structured findings.
Adapting for other SAST tools:
| Tool | Command pattern |
|---|---|
| gosec | gosec -fmt=sarif -out=results.sarif ./... || true |
| Semgrep | semgrep --sarif --output=results.sarif . || true |
| CodeQL | Run via github/codeql-action/analyze with output: results.sarif, then a separate cilock step that captures the SARIF |
| Trivy fs | trivy fs --format sarif -o results.sarif . |
Pattern 3: SBOM from a container imageโ
For OCI images, generate the SBOM from the saved image tarball alongside the build:
- name: docker-build + sbom
uses: aflock-ai/cilock-action@v1.0.1
with:
step: docker-build
command: |
bash -c "
docker buildx build --metadata-file docker-metadata.json -t myapp:test -o type=docker,dest=image.tar . &&
syft image.tar -o cyclonedx-json=image-bom.cdx.json
"
attestations: environment git github sbom oci docker
cilock-args: --attestor-product-include-glob "{docker-metadata.json,image-bom.cdx.json,image.tar}"
Now the attestation includes the buildx metadata (docker), the OCI image content (oci), and the SBOM (sbom), all signed together.
Verifying SBOM and SARIF presence in policyโ
The whole point is making absence a build-blocker. A policy fragment that requires both:
{
"steps": {
"build": {
"name": "build",
"attestations": [
{ "type": "https://aflock.ai/attestations/material/v0.1" },
{ "type": "https://aflock.ai/attestations/command-run/v0.1" },
{ "type": "https://aflock.ai/attestations/product/v0.1" },
{ "type": "https://aflock.ai/attestations/sbom/v0.1" }
],
"functionaries": [{ "type": "publickey", "publickeyid": "<your-key>" }]
},
"sast": {
"name": "sast",
"attestations": [
{ "type": "https://aflock.ai/attestations/command-run/v0.1" },
{ "type": "https://aflock.ai/attestations/sarif/v0.1" }
],
"functionaries": [{ "type": "publickey", "publickeyid": "<your-key>" }]
}
}
}
If the build step ran but the SBOM file wasn't produced (or wasn't captured by the product glob), there's no sbom attestation in the collection and cilock verify fails the step.
A subtle but important distinctionโ
sarif proves a SAST tool ran and captures its findings. It does not prove the tool passed with zero findings. To enforce "no high-severity findings," combine the SARIF attestor with an OPA Rego rule:
package sast.results
import rego.v1
deny contains msg if {
some run in input.report.runs
some result in run.results
result.level == "error"
msg := sprintf("SARIF error-level finding: %s", [result.message.text])
}
Note the input.report.runs path: the SARIF attestor wraps the SARIF document under a report field alongside reportFileName and reportDigestSet, so the policy needs to traverse one extra level versus a bare SARIF file.
Embed the base64-encoded module under regopolicies for the sarif attestation in your policy. Same model for SBOM-based rules (e.g., "deny if the SBOM contains a known-bad component", traversing input.components[] on the CycloneDX BOM).
See alsoโ
- SBOM attestor schema upstream
- SARIF attestor schema upstream
- GitHub Actions tutorial, full 5-step pipeline using these patterns