DevSecOps (AppSec in the pipeline) · lesson 4 of 7
Beginner
3Intermediate
3Also part of DevSecOps (AppSec in the pipeline)
10- + Bash Security Best Practices from Bash
- + Secrets: Real Log Masking, and the Base64 Trick That Defeats It from GitHub Actions
- + Docker Security from Docker
- + Multi-Stage Builds from Docker
- + sensitive = true: What It Hides, and What It Doesn't from Terraform
- + Variable Precedence: host_vars Beats group_vars, and extra-vars Beats Everything from Ansible
- + Scanning Terraform for Misconfiguration with Checkov from Cloud Security
- + Policy as Code: Testing Terraform Plans with OPA and Conftest from Cloud Security
- + MCP Authorization: OAuth 2.1, CIMD and a Real Identity Provider from MCP (Model Context Protocol)
- + Running MCP in Production: Scaling, Caching and Operations from MCP (Model Context Protocol)
Also part of: Web Security (OWASP Top 10, fix-side)
Dependency Scanning (SCA) and SBOMs with pip-audit, Syft and Grype
Pin an old requests release, read the advisories pip-audit reports, build a CycloneDX SBOM with Syft, scan it with Grype, then bump the pin to a clean run.
By SkillByExample · Published
Most of the code in a typical application comes from its dependencies, and their vulnerabilities
are published every day. This lesson pins a deliberately old library, finds its advisories two
ways, captures the dependency list as an SBOM you can re-scan later, and fixes the pin. Tested with
pip-audit 2.10.1 on Python 3.12.15 (python:3.12-slim), Syft 1.54.1 (anchore/syft:v1.54.1),
Grype 0.120.1 (anchore/grype:v0.120.1) and Docker Engine 29.8.2.
Run date: 2026-10-14. Vulnerability databases change daily, so on a later date you may see more advisories, different identifiers or different EPSS scores. The method doesn’t change.
The lab
pip-audit runs in a small Python image. Syft and Grype run from their official images.
# Dockerfile
FROM python:3.12-slim
RUN pip install -q pip-audit==2.10.1
WORKDIR /w
$ docker build -q -t sca-lab .
sha256:8e6ffcafa19a48089dc6ba82c787066d046d6480059578e6922b154e4c07d3e1
The project is one pinned dependency in an app/ folder: requests 2.31.0, released in May 2023.
# requirements.txt
requests==2.31.0
Audit the requirements
pip-audit resolves the requirements and looks each package up in the PyPI advisory database.
--aliases adds the matching CVE and GitHub advisory IDs.
$ docker run --rm -v "$PWD/app":/w sca-lab pip-audit -r requirements.txt --aliases
Name Version ID Fix Versions Aliases
-------- ------- --------------- ------------ -----------------------------------
requests 2.31.0 PYSEC-2026-1873 2.32.0 GHSA-9wx4-h78v-vm56, CVE-2024-35195
requests 2.31.0 PYSEC-2026-1872 2.32.4 CVE-2024-47081, GHSA-9hjg-9r4m-mvj7
requests 2.31.0 PYSEC-2026-2275 2.33.0 CVE-2026-25645, GHSA-gc5v-m9x4-r6x2
requests 2.31.0 PYSEC-2026-1873 2.32.0 GHSA-9wx4-h78v-vm56, CVE-2024-35195
requests 2.31.0 PYSEC-2026-1872 2.32.4 CVE-2024-47081, GHSA-9hjg-9r4m-mvj7
requests 2.31.0 PYSEC-2026-2275 2.33.0 CVE-2026-25645, GHSA-gc5v-m9x4-r6x2
Found 6 known vulnerabilities in 1 package
$ echo $?
1
“6 known vulnerabilities”, but read the ID column: there are three distinct advisories, each listed twice. On this run date every pip-audit invocation printed each row twice, so count distinct IDs, not rows. The column that matters is Fix Versions: the three fixes land in 2.32.0, 2.32.4 and 2.33.0, so nothing below 2.33.0 clears all of them. The order of the aliases also changes between runs, so don’t diff this table as text. Exit code 1 is what fails a build.
Capture the dependency list as an SBOM
Syft inventories a directory or image. Its default table shows what it found.
$ docker run --rm -v "$PWD/app":/src anchore/syft:v1.54.1 dir:/src -q
NAME VERSION TYPE
requests 2.31.0 python
Now write the same inventory as CycloneDX JSON. -o format=path writes a file, here into the
mounted folder.
$ docker run --rm -v "$PWD/app":/src anchore/syft:v1.54.1 dir:/src -q -o cyclonedx-json=/src/sbom.json
$ echo $?
0
A short script shows what’s inside the file.
# show_sbom.py
import json
with open("sbom.json") as f:
bom = json.load(f)
print(bom["bomFormat"], bom["specVersion"])
for c in bom["components"]:
print(c["type"], c["name"], c.get("version", "-"), c.get("purl", "-"))
$ docker run --rm -v "$PWD/app":/w sca-lab python show_sbom.py
CycloneDX 1.7
library requests 2.31.0 pkg:pypi/requests@2.31.0
file /src/requirements.txt - -
Two components: the library, identified by its package URL (pkg:pypi/requests@2.31.0), and
the file it was found in. The purl is what makes an SBOM useful: it names the exact package in a
form any scanner can look up, independent of the language tool that produced it.
Scan the SBOM, not the code
Grype scans an SBOM directly with the sbom: prefix, so it never needs the source or a rebuild.
The named volume keeps Grype’s database between runs. --fail-on medium sets the exit code.
$ docker run --rm -v grype-db:/.cache/grype -v "$PWD/app":/src anchore/grype:v0.120.1 sbom:/src/sbom.json -q --fail-on medium
NAME INSTALLED FIXED IN TYPE VULNERABILITY SEVERITY EPSS RISK
requests 2.31.0 2.32.4 python GHSA-9hjg-9r4m-mvj7 Medium 1.0% (61st) 0.5
requests 2.31.0 2.32.0 python GHSA-9wx4-h78v-vm56 Medium 0.3% (25th) 0.2
requests 2.31.0 2.33.0 python GHSA-gc5v-m9x4-r6x2 Medium 0.2% (7th) < 0.1
$ echo $?
2
The same three advisories, this time once each and under their GitHub IDs, from a different
database. Grype adds EPSS, the estimated probability that a vulnerability is exploited in the
next 30 days, and a combined risk score, which help decide what to fix first. Note the exit code:
2, not 1. Without --fail-on, Grype exits 0 even with findings.
The first run downloads Grype’s database (Grype’s db status reported it built at
2026-10-14T06:30:34Z for this run). The SBOM, though, is a plain file: store it with each
release, and a year from now you can re-scan exactly what shipped against that day’s database.
Fix the pin and re-run both
Change the pin to the highest fixed version from the table.
# requirements.txt
requests==2.33.0
$ docker run --rm -v "$PWD/app":/w sca-lab pip-audit -r requirements.txt --aliases
No known vulnerabilities found
$ echo $?
0
$ docker run --rm -v "$PWD/app":/src anchore/syft:v1.54.1 dir:/src -q -o cyclonedx-json=/src/sbom.json
$ docker run --rm -v grype-db:/.cache/grype -v "$PWD/app":/src anchore/grype:v0.120.1 sbom:/src/sbom.json -q --fail-on medium
No vulnerabilities found
$ echo $?
0
Both clean, both exit 0. The fix for all three advisories was a one-line version bump, which is typical: most dependency vulnerabilities are already fixed upstream by the time a scanner reports them, and the work is keeping pins current. Re-run your tests after any bump, since a security fix can still change behaviour.
Practice
-
Pin
requests==2.32.0instead and run pip-audit. Which advisories remain?Solution
2.32.0 fixes only the advisory whose fix version is 2.32.0 (CVE-2024-35195). The two with fix versions 2.32.4 and 2.33.0 remain, so pip-audit still exits 1. “Upgraded” isn’t the same as “fixed”: compare against the highest fix version in the table.
-
Make Grype fail only on High or Critical findings. What exit code do you get on the original SBOM, and why?
Solution
Use
--fail-on high. All three findings were Medium on this run date, so Grype prints the same table and exits 0. Choosing the threshold is a policy decision; write it down next to the CI job rather than leaving it implicit. -
Ask pip-audit for JSON (
-f json) and print each distinct vulnerability ID once.Solution
import json, subprocess out = subprocess.run(["pip-audit", "-r", "requirements.txt", "-f", "json"], capture_output=True, text=True).stdout ids = {v["id"] for d in json.loads(out)["dependencies"] for v in d.get("vulns", [])} print(sorted(ids))A set removes the duplicate rows, and sorting makes the output stable enough to compare between runs.
-
Generate an SPDX SBOM instead of CycloneDX and scan it with Grype.
Solution
Use
-o spdx-json=/src/sbom.spdx.jsonin the Syft command andsbom:/src/sbom.spdx.jsonin the Grype command. Grype reads both formats and reports the same findings, because both carry the same package URL.
Where this fits
SCA and SBOMs cover the code you depend on. The next lesson tests the running application from the outside, with an OWASP ZAP baseline scan.