> Audit the Deployed Set, Not the Lockfile

Security Engineering 入门 2026-07-30 04:54 2026-07-30
#pip-audit #dependencies #CVE #Docker #supply chain #SCA #requirements

Scanning requirements.txt found nothing, because it could not even install. Scanning what the running container actually had installed found 117 CVEs across 15 packages — and the file being scanned was the wrong file anyway.

Audit the Deployed Set, Not the Lockfile

The obvious first move when auditing dependencies:

pip-audit -r requirements.txt

On this project it failed outright. pip-audit resolves a requirements file by attempting installation into a temp environment, and psycopg2 needs pg_config to build from source. No PostgreSQL headers on the machine, no audit.

The easy reaction is to install the build dependency and move on. That would have audited the wrong thing.

Two ways the file lies

1. The file is not what gets installed. The Dockerfile line was:

RUN pip install --no-cache-dir -r requirements_dev.txt

Not requirements.txt. Both files existed, both looked plausible, and the one I would naturally have audited — and edited — had no effect on the image at all. I only caught this because a rebuild produced identical package versions and I checked pip freeze before swapping the live container.

2. A requirements file is a request, not a result. Ranges resolve to whatever was newest at build time. Transitive dependencies do not appear at all. An image built three months ago contains what the resolver picked then, which no file in the repo records.

Audit the artifact

# ask the running container what it actually has
docker exec mycms_web pip freeze > /tmp/deployed.txt

# --no-deps: these are resolved, pinned versions - do not re-resolve them
pip-audit -r /tmp/deployed.txt --no-deps

--no-deps is what makes this work. Every line is already an exact pin from a real environment, so there is nothing to resolve and nothing to build. No pg_config, no compiler, no network resolution.

Result on this site: 117 known CVEs across 15 packages. Django, Pillow, cryptography, urllib3, requests — the ordinary long tail of an image nobody had rebuilt in a while.

Verify the fix against the artifact too

After bumping pins, the same principle applies in reverse. Do not trust that the build did what you asked:

docker build -t mycms_web:candidate .
docker run --rm --entrypoint pip mycms_web:candidate freeze | grep -E '^(Django|Pillow)='

Check before swapping the live container. My first rebuild was a complete no-op — I had edited requirements.txt, the image installs requirements_dev.txt, and every version came back unchanged. Had I not checked the candidate image first, I would have restarted the service, seen it come up healthy, and recorded the CVEs as fixed.

The general principle

Question Wrong source Right source
What versions am I running? requirements.txt pip freeze in the container
Is DEBUG off? docker-compose.yml settings.DEBUG in the process
Are security headers set? nginx.conf curl -I against each path

Config files record intent. Audit the artifact. Every finding in this audit came from the right-hand column; the left-hand column is what made each bug survive as long as it did.

Related: the typo that shipped DEBUG=True · nginx add_header does not inherit