Chapter 26Lesson 02~320 minutes

SBOMs, Dependency Lists, Vulnerability Management, Remediation, and Security Dashboards: Guided Hands-On Workflow and Core Operations

Create a Free-compatible synthetic SBOM and vulnerability evidence workflow, then map it to current Ultimate Dependency List and vulnerability-management surfaces.

Hands-onSBOM fixtureEvidence hashRemediation MRFree path

Learning objectives

  • Create and validate a synthetic CycloneDX SBOM without real vulnerable packages or secrets.
  • Map a synthetic finding to the exact purl/version in the SBOM and prove both file identities with hashes.
  • Create a remediation branch/MR that changes the component version and produces before/after evidence.
  • Use fixture/read-only mappings for Ultimate Dependency List, vulnerability report, and export/API surfaces.
  • Clean up vulnerable training data while retaining only sanitized evidence.
Availability baseline — verified 2026-08-22 against GitLab 19.3. A plain CycloneDX JSON file is an open, portable evidence format and can be generated, validated, hashed, stored as an ordinary CI artifact, and reviewed on GitLab Free. GitLab’s artifacts:reports:cyclonedx ingestion, Dependency List, Dependency Scanning, Vulnerability Report/details, Security Dashboards, Security Inventory, vulnerability exports/APIs, and hosted vulnerability-management lifecycle are currently Ultimate across GitLab.com, Self-Managed, and Dedicated. Therefore every mandatory lab in this chapter has a Free-compatible fixture path; Ultimate steps are clearly optional/read-only. GitLab 19.3 is the current monthly release, published 2026-08-20.

1. Disposable scenario and preflight

Create a disposable project named gitlab-ch26-sbom-lab. The required path needs only GitLab Free, Git, Python 3 for local JSON validation, and either a hosted runner or the no-runner inspection path. The package names and CVE identifiers below are deliberately fictional; do not introduce a real known-vulnerable dependency merely to create a training finding.

Safety boundary. The lab never uses a real secret, real CVE exploit, production repository, package registry credential, or security-policy bypass. The finding fixture says synthetic: true. If you use an Ultimate project, the hosted vulnerability-management steps are read-only or performed only on this disposable project.
mkdir gitlab-ch26-sbom-lab && cd gitlab-ch26-sbom-lab
git init
git switch -c main
mkdir -p evidence
printf 'widget-parser=1.2.0\n' > deps.lock

2. Build a tiny SBOM with deterministic identity

Create an ordinary CycloneDX JSON document. On the Free path it is deliberately stored as a normal artifact, not as artifacts:reports:cyclonedx, because the typed CycloneDX report integration is currently Ultimate.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.6",
  "serialNumber": "urn:uuid:00000000-0000-4000-8000-000000000026",
  "version": 1,
  "metadata": {
    "component": {"type": "application", "name": "gitlab-ch26-sbom-lab", "version": "1.0.0"}
  },
  "components": [{
    "type": "library",
    "name": "widget-parser",
    "version": "1.2.0",
    "purl": "pkg:npm/widget-parser@1.2.0",
    "properties": [{"name": "training.synthetic", "value": "true"}]
  }]
}
python -m json.tool evidence/sbom-before.cdx.json >/dev/null
sha256sum evidence/sbom-before.cdx.json
# Record the digest in notes/evidence-log.md; do not merely trust the filename.

3. Map a finding to the same component identity

The finding fixture is not a GitLab Secure report and is not submitted to a vulnerability database. It is a teaching object that makes the mapping explicit.

{
  "id": "SYNTH-CVE-2099-0001",
  "synthetic": true,
  "severity": "high",
  "status": "needs_triage",
  "component": {
    "name": "widget-parser",
    "version": "1.2.0",
    "purl": "pkg:npm/widget-parser@1.2.0"
  },
  "evidence": {"source": "training-fixture", "reachable": "unknown"}
}

Verify the purl and version match between the SBOM and finding. If they do not, you have evidence about two different component identities.

python - <<'PY'
import json
s=json.load(open('evidence/sbom-before.cdx.json'))['components'][0]
f=json.load(open('evidence/finding-before.json'))['component']
assert (s['purl'],s['version']) == (f['purl'],f['version'])
print('identity-match', s['purl'])
PY

4. Preserve evidence in a Free-compatible pipeline

The pipeline validates the files and emits their hashes. It does not need package installation, internet access, privileged runners or secrets.

stages: [verify]

verify_evidence:
  stage: verify
  image: python:3.13-alpine
  script:
    - python -m json.tool evidence/sbom-before.cdx.json >/dev/null
    - python -m json.tool evidence/finding-before.json >/dev/null
    - sha256sum evidence/sbom-before.cdx.json evidence/finding-before.json > evidence/SHA256SUMS
  artifacts:
    when: always
    paths:
      - evidence/
    expire_in: 7 days

On GitLab Free, inspect the job SHA, pipeline ID, artifact list, and SHA256SUMS. If no runner is available, use CI Lint plus the same commands locally and record the expected artifact tree as a fixture. The lesson objective is causal evidence, not spending compute.

5. Optional Ultimate mapping: Dependency List and vulnerability record

If an Ultimate disposable project is available, the same conceptual evidence maps to hosted surfaces, but the live scanners and report schemas must be valid. Current Dependency List ingestion expects CycloneDX 1.4–1.6 from the latest default-branch pipeline. The Vulnerability Report and status-changing workflow are Ultimate.

Free fixture Optional Ultimate surface What to compare
sbom-before.cdx.json Secure → Dependency list Component name/version/purl and default-branch pipeline freshness.
finding-before.json Secure → Vulnerability report/details Finding identity, component/location, severity, scanner and status.
Evidence log Vulnerability activity Who changed status, when, dismissal reason/comment, no-longer-detected activity.
Local CSV/JSON fixture Vulnerability export/API Pagination/export authorship/permission and lifecycle values.

Do not fabricate a typed cyclonedx or dependency_scanning report merely to make a dashboard populate. A valid hosted result must come from current supported schemas and scanner integration.

6. Remediate through a controlled branch and MR

Use a fictional fixed version 1.2.1. This models the operational sequence without downloading a package.

git switch -c remediate/widget-parser
printf 'widget-parser=1.2.1\n' > deps.lock
# Copy the SBOM fixture to sbom-after.cdx.json and change only version/purl to 1.2.1.
# Create finding-after.json with an empty findings array or a training disposition of no_longer_detected.
git add deps.lock evidence/
git commit -m "ch26: remediate synthetic dependency"
git rev-parse HEAD

Open a merge request. Before merging, predict: the branch SHA changes; default-branch hosted state does not change yet; the after-SBOM should contain pkg:npm/widget-parser@1.2.1; and a valid rescan should no longer map the synthetic finding to version 1.2.0.

7. Verify before changing lifecycle state

After the remediation pipeline, compare exact values rather than saying “the scan is green.”

python - <<'PY'
import json
before=json.load(open('evidence/sbom-before.cdx.json'))['components'][0]
after=json.load(open('evidence/sbom-after.cdx.json'))['components'][0]
print('before', before['purl'])
print('after ', after['purl'])
assert before['version']=='1.2.0'
assert after['version']=='1.2.1'
PY

If using Ultimate, No longer detected activity after the default-branch scan is a signal to verify the fix. Then an authorized Security Manager/Maintainer/Owner may set the vulnerability to Resolved. Do not jump directly from “scanner omitted it” to “risk resolved.”

If the team instead decides the finding is acceptable risk or a false positive, the lifecycle state is Dismissed with a required reason/comment—not Resolved. That decision leaves the vulnerability record for audit and must not be confused with a successful remediation.

8. Challenge: which control belongs where?

You have three facts: the SBOM still shows 1.2.0, the finding fixture says 1.2.1, and the dashboard says zero active findings. Which surface should you trust first? Answer: none in isolation. Prove the pipeline/SHA and artifact hashes, then reconcile component identities. A clean dashboard cannot repair contradictory evidence.

9. Cleanup and evidence retention

Before deleting the branch or project, retain only sanitized evidence: commit SHAs, pipeline/job IDs, SBOM hashes, fictional purls, MR URL/ID, and the decision record. Do not retain real credentials or unnecessary vulnerable binaries.

git switch main
git branch -D remediate/widget-parser
# If pushed, delete only the disposable remote branch after verifying the MR/evidence record.
# Delete the disposable GitLab project only when the evidence exercise is complete.

Knowledge check

Why is the Free lab SBOM stored as an ordinary artifact instead of artifacts:reports:cyclonedx?

What proves the finding maps to the SBOM component?

Why use a fictional CVE and package?

After the remediation branch pipeline, can the default-branch vulnerability be considered fixed automatically?

A vulnerability API returns 403. What should you inspect first?

Summary and next bridge

You now have a reproducible evidence chain that works without paid features: inventory → finding fixture → remediation change → after-inventory → disposition. Lesson 3 turns that mechanics into architecture decisions for real teams.

Primary sources and version notes

These lessons were finalized against current official GitLab documentation on 2026-08-22. Re-check tier, feature-flag, API, and Self-Managed-version behavior before using the same workflow later.

Next lesson

Configuration, Design Choices, and Tradeoffs

Turn the workflow into design rules for inventory, remediation automation, dismissals and aggregate security views.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.