Safeguard
Developer Tools•

How do you scan unpinned Python requirements in a legacy monorepo?

Unpinned requirements files make scan results approximate because pip resolves versions at install time. How to pin, compare and report honestly.

Safeguard Security Team
3 min read

Treat unpinned requirements files as approximate. Pip chooses versions when it installs, so a scanner reading "requests" with no version cannot know what was deployed. Resolve once in a clean environment, save the result as pinned versions, scan that, and report which directories were pinned and which were not.

Why unpinned files are a problem for scanning

Pip's resolver does real work at install time. Its documentation states that the resolver is capable of backtracking, meaning it can discard a choice and try an earlier version to satisfy all constraints. The same file can therefore yield different installs on different days, as new releases appear.

For a scanner this means a manifest lists intent, not fact. Two scans a week apart can disagree without any code change.

Steps

  1. Find the files. Search for requirements files, pyproject.toml and setup.py in each service directory. Legacy repositories often hold several requirements files per service, such as base, dev and prod.
  2. Pick the one that production uses. If you cannot tell, ask the owning team or read the Dockerfile or deployment script.
  3. Build a clean virtual environment with the Python version production uses, then install from that file.
  4. Export the installed versions to a pinned file.
  5. Scan the pinned file and compare with a scan of the original. Note any package that differs.
  6. Commit the pinned file beside the original, or in place of it if the team agrees.

A simple export command, run inside the clean environment:

pip freeze > requirements.pinned.txt

Where the answer differs

CaseHandling
Production image still availableRead installed packages from the image; that is the real answer
Only the file existsResolve in a clean environment; label as reconstructed
Pinned with hashes alreadyScan as is; this is the best case
Multiple Python versions in useResolve once per version; results may differ

The first row is worth remembering: if a built container image exists, it holds the true list. Compare the image with your reconstructed file to see how far apart they are.

Keep reports honest

In the coverage map, add a column for pin status: exact, reconstructed or unpinned. When you present the first scan to leadership, show how much of the result rests on reconstruction. It prevents a later argument when a deployed version turns out to differ.

Avoid a common trap

Do not fix unpinned files by pinning to whatever pip installs today in a service nobody runs tests for. A newer resolution can break the service. For frozen services, leave the file alone and record the limit instead.

Where Safeguard fits

Safeguard's dependency scanning can read pinned and unpinned files and its SBOM output records what was found, and reachability analysis helps decide which findings matter. Try the free scan at https://gold.safeguard.sh/scan.

Quick check

Are hashes needed for scanning?

No. Hashes help integrity checks at install time. For scanning, exact versions are what matter.

Should I pin every transitive package?

For services you rebuild, yes, because a freeze captures them. For frozen services, record the limit instead of changing the file.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.