Safeguard
Guides

How to Generate an SBOM in a GitLab CI Pipeline

A working .gitlab-ci.yml for SBOM generation with Syft: CycloneDX report artifacts, a Grype scan stage, and Cosign attestations pushed next to the image.

Priya Raman
Staff Security Engineer
5 min read

The shortest path to an SBOM in GitLab CI is a job that runs Syft against your freshly built container image, emits CycloneDX JSON, and registers it with artifacts:reports:cyclonedx — about twelve lines of YAML. Everything past that is refinement: scanning the SBOM instead of re-scanning the image, signing it as an attestation, and deciding what to do about source-versus-container drift. Here is the whole pipeline, in the order you should build it.

The minimal job

Assume a standard docker-build stage that pushes $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA. Add:

sbom:
  stage: test
  image:
    name: anchore/syft:v1.0.1
    entrypoint: [""]
  script:
    - /syft $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
        -o cyclonedx-json=sbom.cdx.json
        -o spdx-json=sbom.spdx.json
  artifacts:
    reports:
      cyclonedx: sbom.cdx.json
    paths:
      - sbom.cdx.json
      - sbom.spdx.json
    expire_in: 1 year

Two things worth noticing. The reports:cyclonedx declaration is not decorative — GitLab ingests the file into the dependency list (Ultimate tier), so components become queryable in the UI rather than trapped in a JSON blob. And emitting both CycloneDX and SPDX costs one extra flag now but saves an argument later; customers and regulators are split on format, so produce both while the build context is hot.

Pin the Syft version. SBOM generators change their component-detection logic between releases, and an unpinned generator gives you diffs that look like supply chain changes but are actually tool changes.

If you are on Ultimate, mind the overlap

GitLab's own Dependency Scanning (the Gemnasium analyzers) has emitted CycloneDX reports since 15.7, per lockfile it understands. That covers declared application dependencies well, but a container SBOM from Syft additionally captures OS packages (apk, deb, rpm), language runtimes, and binaries that never appear in a lockfile — which is exactly where things like an outdated openssl hide. The pragmatic setup is both: keep Dependency-Scanning.gitlab-ci.yml included for MR widgets, and generate the image-level SBOM as the artifact of record for releases.

Scan the SBOM, not the image, in the next stage

Once the SBOM exists, downstream jobs should consume it instead of re-analyzing the image. It is faster (no image pull, no filesystem walk) and it guarantees every tool reasons about the same inventory:

vuln-scan:
  stage: test
  needs: ["sbom"]
  image:
    name: anchore/grype:v0.74.7
    entrypoint: [""]
  script:
    - /grype sbom:./sbom.cdx.json --fail-on high

--fail-on high blocks the pipeline on high and critical findings. Start with --fail-on critical if the first run produces a wall of red — a gate the team immediately mutes protects nothing. Grype honors a .grype.yaml ignore file, which is where accepted risks go, with a comment and an owner, under code review like everything else.

Sign the SBOM and attach it to the image

An unsigned SBOM sitting in a job artifact is a claim; a signed attestation attached to the image digest is evidence. Cosign with GitLab's OIDC-backed keyless signing:

sbom-attest:
  stage: release
  needs: ["sbom"]
  image: alpine:3.19
  id_tokens:
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - apk add --no-cache cosign crane
    - DIGEST=$(crane digest $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA)
    - cosign attest --yes
        --predicate sbom.cdx.json
        --type cyclonedx
        $CI_REGISTRY_IMAGE@$DIGEST

Note the digest resolution step: always attest the digest, never the tag. Tags move; the attestation must bind to the exact bytes you shipped. Verification on the consuming side — admission controllers, customer audits — is covered in our piece on Cosign verification policies in production.

Source SBOM vs. container SBOM

Run Syft in dir: mode against the repo and you get the declared inventory; run it against the image and you get the shipped one. They will differ, and the differences are informative: build-time-only tools, OS packages, binaries copied in multi-stage builds, and occasionally a dependency your build injects that no lockfile mentions. Generating both and diffing them (syft dir:. -o cyclonedx-json=source.cdx.json, then compare component lists) is a cheap tripwire for build-process tampering — the class of problem where the artifact stops matching the source.

At one or two services, eyeballing that diff works. At fifty services with per-commit SBOMs, you have a management problem, not a generation problem — storage, versioning, cross-service queries, and drift alerts are what SBOM Studio exists for; Safeguard ingests exactly the CycloneDX artifacts this pipeline produces. For the underlying concepts, the Academy has a self-paced SBOM track.

Frequently asked questions

Should I generate the SBOM from the repo or from the image?

From the image, if you pick only one — it reflects what actually ships, including OS packages and binaries no lockfile declares. Generate both when you can, because the diff between them is itself a useful integrity signal.

CycloneDX or SPDX?

Emit both; it is one extra output flag. CycloneDX tends to dominate security tooling and vulnerability workflows, SPDX shows up more in license compliance and procurement contexts. Committing to one format in 2024 is an unforced error.

How long should SBOM artifacts be retained?

Match your artifact retention to your support window — if customers run a release for three years, keep its SBOM for three years. expire_in: 1 year is a floor for CI artifacts; the SBOM of record for releases belongs in longer-term storage, ideally attached to the image as an attestation.

Does an SBOM slow the pipeline down noticeably?

Syft against an already-pulled image typically runs in seconds to low tens of seconds for normal service images. Scanning the SBOM with Grype instead of re-scanning the image usually makes the overall pipeline faster than a naive scan-everything setup.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.