SBOM Generation, Dependency Lists, Vulnerability Reports, Policy Evaluation, and Supply-Chain Evidence: Guided Hands-On Workflow and Core Operations
Generate a CycloneDX 1.6 SBOM in a disposable Python lab, hash the built artifact and SBOM, correlate a synthetic vulnerability report, apply an explicit policy, and retain an evidence bundle.
Learning objectives
- Create a disposable Python artifact and record its SHA-256 digest.
-
Generate a CycloneDX 1.6 SBOM with pinned
cyclonedx-bom==7.3.1in a virtual environment. - Create and validate a synthetic vulnerability-correlation report without relying on a changing live advisory service.
- Evaluate an explicit local policy and retain the inputs/result as normal artifacts.
- Prove that source SHA, artifact digest, SBOM, finding, and policy result all describe the same lab run.
1. Disposable scenario and preflight
Create a throwaway project named glci-ch26-sbom-lab.
The lab uses synthetic source and a synthetic advisory so the result
remains deterministic. No production package registry, cloud
account, vulnerability database, or real credential is required.
Assumptions used in the examples: Python 3.13-compatible tooling,
cyclonedx-bom==7.3.1, CycloneDX 1.6 output, and
ordinary GitLab job artifacts. If your runner image differs, record
it in the evidence packet rather than hiding the difference.
CI_PIPELINE_SOURCE, CI_COMMIT_SHA,
pipeline/job IDs, and do not substitute a production artifact or
live secret into the exercise.
2. Small source and deterministic build artifact
glci-ch26-sbom-lab/
├── app/
│ └── message.txt
├── requirements.txt
├── policy/
│ └── evaluate.py
├── tools/
│ └── make_synthetic_findings.py
└── .gitlab-ci.yml
# app/message.txt
chapter26 supply-chain evidence lab
# requirements.txt
requests==2.32.5
The “application artifact” is intentionally tiny. What matters is that the bytes are stable and hashable.
mkdir -p dist evidence
cp app/message.txt dist/app.txt
sha256sum dist/app.txt | tee evidence/artifact.sha256
printf 'source_sha=%s\n' "$CI_COMMIT_SHA" | tee evidence/build-identity.txt
printf 'pipeline_id=%s job_id=%s source=%s\n' \
"$CI_PIPELINE_ID" "$CI_JOB_ID" "$CI_PIPELINE_SOURCE" >> evidence/build-identity.txt
3. Generate CycloneDX 1.6 in an isolated virtual environment
cyclonedx-bom is a Python package; its CLI entry point
is cyclonedx-py. Pin the generator version because
generator behavior is part of the evidence.
python -m venv .venv
. .venv/bin/activate
python -m pip install --disable-pip-version-check 'cyclonedx-bom==7.3.1'
cyclonedx-py --version | tee evidence/cyclonedx-generator-version.txt
cyclonedx-py requirements requirements.txt \
--spec-version 1.6 \
--output-format json \
--output-file evidence/sbom.cdx.json
sha256sum evidence/sbom.cdx.json | tee evidence/sbom.sha256
python -m json.tool evidence/sbom.cdx.json >/dev/null
Version 7.x removed older deprecated CLI switches such as
--schema-version and --outfile; current
syntax uses --spec-version and
--output-file. If you upgrade the tool, run
cyclonedx-py requirements --help and record the new
version before changing the course command.
4. Inspect the SBOM rather than trusting the filename
from pathlib import Path
import json
bom = json.loads(Path("evidence/sbom.cdx.json").read_text())
assert bom["bomFormat"] == "CycloneDX"
assert bom["specVersion"] == "1.6"
components = bom.get("components", [])
print("component_count=", len(components))
for component in components:
print(component.get("name"), component.get("version"), component.get("purl", "<no-purl>"))
This step proves that a machine-readable inventory exists and that its schema/version is the intended one. It still does not prove the package is vulnerable or safe.
5. Correlate a synthetic vulnerability deterministically
A live vulnerability database changes over time, which is useful operationally but poor for a deterministic teaching checkpoint. Instead, the lab uses a synthetic advisory that deliberately matches one PURL from the SBOM.
# tools/make_synthetic_findings.py
from pathlib import Path
import json
bom = json.loads(Path("evidence/sbom.cdx.json").read_text())
purls = {c.get("purl") for c in bom.get("components", [])}
target = "pkg:pypi/requests@2.32.5"
findings = []
if target in purls:
findings.append({
"id": "CVE-LAB-2026-0001",
"source": "chapter26-synthetic-db-1",
"component": target,
"severity": "HIGH",
"status": "detected",
"fixed_version": "2.32.6-LAB"
})
report = {"database":"chapter26-synthetic-db-1", "findings":findings}
Path("evidence/vulnerabilities.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
print(f"findings={len(findings)}")
python tools/make_synthetic_findings.py
python -m json.tool evidence/vulnerabilities.json >/dev/null
sha256sum evidence/vulnerabilities.json | tee evidence/vulnerabilities.sha256
6. Apply policy as a separate, reviewable decision
# policy/evaluate.py
from pathlib import Path
import json, sys
report = json.loads(Path("evidence/vulnerabilities.json").read_text())
blocked = [f for f in report.get("findings", []) if f.get("severity") in {"HIGH", "CRITICAL"}]
result = {
"policy": "chapter26-no-unexcepted-high-v1",
"database": report.get("database"),
"decision": "block" if blocked else "allow",
"blocking_ids": [f["id"] for f in blocked],
}
Path("evidence/policy-result.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
print(json.dumps(result))
sys.exit(1 if blocked else 0)
The first run should block. That is expected evidence, not a broken scanner. The generator succeeded, the SBOM was valid, the synthetic correlator found one issue, and the policy independently decided that the issue is not currently acceptable.
7. Put the workflow into a bounded GitLab pipeline
stages: [build, evidence, policy]
build_artifact:
stage: build
image: python:3.13-alpine
script:
- mkdir -p dist evidence
- cp app/message.txt dist/app.txt
- sha256sum dist/app.txt | tee evidence/artifact.sha256
- printf 'source_sha=%s\npipeline_id=%s\nsource=%s\n' "$CI_COMMIT_SHA" "$CI_PIPELINE_ID" "$CI_PIPELINE_SOURCE" > evidence/build-identity.txt
artifacts:
expire_in: 7 days
paths: [dist/, evidence/]
sbom_and_correlation:
stage: evidence
image: python:3.13-alpine
needs:
- job: build_artifact
artifacts: true
script:
- python -m venv .venv
- . .venv/bin/activate
- python -m pip install --disable-pip-version-check 'cyclonedx-bom==7.3.1'
- cyclonedx-py --version | tee evidence/cyclonedx-generator-version.txt
- cyclonedx-py requirements requirements.txt --spec-version 1.6 --output-format json --output-file evidence/sbom.cdx.json
- python tools/make_synthetic_findings.py
- sha256sum evidence/sbom.cdx.json evidence/vulnerabilities.json | tee evidence/evidence-SHA256SUMS
artifacts:
when: always
expire_in: 7 days
paths: [dist/, evidence/]
policy_gate:
stage: policy
image: python:3.13-alpine
needs:
- job: sbom_and_correlation
artifacts: true
script:
- python policy/evaluate.py
artifacts:
when: always
expire_in: 7 days
paths: [evidence/]
The mandatory pipeline intentionally uses ordinary artifacts. On
Ultimate, a conforming CycloneDX report can additionally be declared
through artifacts:reports:cyclonedx for GitLab
ingestion, but that is an integration enhancement, not a
prerequisite for the evidence chain.
8. Optional GitLab-native ingestion path (Ultimate)
# Optional integration only; keep ordinary paths as raw downloadable evidence.
artifacts:
paths:
- evidence/sbom.cdx.json
reports:
cyclonedx:
- evidence/sbom.cdx.json
Current GitLab docs place the CycloneDX report type and Dependency List in Ultimate. When enabled, the UI can aggregate dependency data. The raw SBOM should still be retained according to your evidence policy because UI retention and vulnerability state have their own lifecycle.
9. Challenge: choose the layer
The SBOM job passes, but the policy job blocks. Which layer should you change?
- If the component inventory is wrong, fix the SBOM generation/input layer.
- If the synthetic advisory is wrong, fix the vulnerability correlation/data layer and preserve the old report.
- If the finding is real but accepted temporarily, create a bounded policy exception with owner/rationale/expiry; do not suppress the component from the SBOM.
- If the artifact digest differs from the evidence bundle, fix the build/artifact lineage before discussing vulnerability severity.
10. Cleanup
deactivate 2>/dev/null || true
rm -rf .venv dist evidence
# In GitLab, delete only the exact disposable project if you created it solely for this lab.
# Do not bulk-delete artifacts or security records from unrelated projects.
Knowledge check
Why does the lab use a synthetic advisory instead of deliberately pinning a currently vulnerable real package?
A live advisory database and real vulnerability status change over time. A synthetic finding keeps the learning result deterministic and avoids encouraging insecure dependencies.
Why is the policy gate a separate job from SBOM generation?
It separates evidence production from governance. A valid SBOM can exist even when policy blocks, and policy can evolve without changing the inventory generator.
What must be preserved if the first policy run intentionally fails?
Pipeline/job IDs, source SHA, artifact digest, SBOM and its digest, vulnerability report/database identity, policy version/result, and the original logs.
Why keep the SBOM as an ordinary artifact even when GitLab UI ingestion is enabled?
Raw evidence is independently inspectable and has its own retention requirements; UI-derived state can expire, change, or be tier-dependent.
What does matching the SBOM component to an advisory prove?
Only that the vulnerability data source associated that component identity/version with the finding at that time—not that the vulnerable code path is reachable or exploitable.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
Documentation verification date: 2026-09-12. GitLab
currently documents the Dependency List,
artifacts:reports:cyclonedx, Vulnerability Report, and
organization-wide security policies as Ultimate features. The
mandatory course path therefore keeps the SBOM, vulnerability
correlation, policy decision, hashes, and evidence bundle as
ordinary artifacts and local scripts so it remains
Free/disposable-compatible. GitLab's Dependency List can ingest
CycloneDX 1.4, 1.5, or 1.6 documents from the latest default-branch
pipeline. Security-policy evaluation trusts scanner artifact
reports; policy evaluation does not itself prove the authenticity or
integrity of the scanner that produced them. The lab pins
cyclonedx-bom==7.3.1, released 2026-07-23, and requests
CycloneDX 1.6 explicitly. The runnable path intentionally avoids
declaring a CycloneDX report to GitLab so Free users can complete
it. The optional report-ingestion block is labeled Ultimate and
should be tested against the target GitLab instance before adoption.
- Dependency list — official reference.
- Dependency scanning by using SBOM — official reference.
- Continuous dependency scanning — official reference.
- CI/CD artifacts report types — official reference.
- CI/CD YAML syntax — official reference.
- Security scanning results — official reference.
- Vulnerability report — official reference.
- Security policies — official reference.
- Merge request approval policies — official reference.
- Scan execution policies — official reference.
- CycloneDX Python SBOM generator — official reference.
- CycloneDX specification 1.6 — official reference.
- Anchore Syft — official reference.
Current assumptions used in this chapter: Version-sensitive YAML, Runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations must be verified against the exact GitLab, GitLab Runner, tool, and external-system versions used in production. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for reproducible work.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.