Skip to main content

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โ€‹

AttestorWhat it capturesWhen to enable it
sbomParses 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.
sarifParses 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:

  1. Material attestor records digests of source files before the command runs.
  2. The shell runs go build then syft, both write to bin/.
  3. Product attestor digests every file in bin/ (the --attestor-product-include-glob "bin/*" filter).
  4. SBOM attestor scans the products, finds bom.cdx.json, parses it, embeds the SBOM into the attestation.
  5. 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:

ToolCommand pattern
gosecgosec -fmt=sarif -out=results.sarif ./... || true
Semgrepsemgrep --sarif --output=results.sarif . || true
CodeQLRun via github/codeql-action/analyze with output: results.sarif, then a separate cilock step that captures the SARIF
Trivy fstrivy 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โ€‹