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.
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.
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.
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?
Because current typed CycloneDX report integration is Ultimate; the mandatory path must remain Free-compatible.
What proves the finding maps to the SBOM component?
Matching stable component identity—especially purl and version—plus the pipeline/artifact context.
Why use a fictional CVE and package?
To teach evidence flow without intentionally introducing real vulnerable software or exploit material.
After the remediation branch pipeline, can the default-branch vulnerability be considered fixed automatically?
No. Verify the merged/default-branch artifact and rescan evidence first; branch evidence alone does not change production/default-branch state.
A vulnerability API returns 403. What should you inspect first?
Tier and authorization to the vulnerability/security surface, not the JSON syntax. Current vulnerability APIs are Ultimate and permission-gated.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.