Vulnerability Scanning Proficient¶
When you'd use this
Find known vulnerabilities in dependencies and code before attackers do.
Find security weaknesses in code and dependencies — static analysis, dependency audits — before attackers do.
What you'll learn¶
- Why dependency vulnerabilities matter
- Compare versions against advisories (tested)
- Scan a dependency list (tested)
- Static analysis for security (SAST)
- Integrating scanning into CI
Most real breaches don't exploit your code — they exploit a known vulnerability in a dependency you forgot to update. Vulnerability scanning finds these automatically. This is a defensive topic: finding and fixing weaknesses in your own projects. The version-checking logic here is run-verified.
The dependency problem¶
Most risk now lives in third-party packages, not your own code.
A typical Python app has dozens of direct dependencies and hundreds of transitive ones. When a vulnerability (a CVE) is disclosed in one of them, you're exposed until you upgrade. The famous cases (Log4Shell, various requests/urllib3 issues) affected millions of apps that simply hadn't updated. Scanning tells you which of your dependencies have known issues.
Version comparison (tested)¶
Version comparison in Vulnerability Scanning — what it is and when to use it.
At its core, a scanner compares your installed version against the version where a vulnerability was fixed. Runnable:
def parse_version(v: str) -> tuple:
return tuple(int(p) for p in v.split("."))
def is_vulnerable(installed: str, fixed_in: str) -> bool:
"""True if installed is older than the version that fixed the issue."""
return parse_version(installed) < parse_version(fixed_in)
# A CVE fixed in requests 2.31.0
print(is_vulnerable("2.25.1", "2.31.0")) # older -> vulnerable
print(is_vulnerable("2.31.0", "2.31.0")) # patched
print(is_vulnerable("2.32.0", "2.31.0")) # newer -> safe
Output:
Comparing version tuples (not strings) correctly orders 2.25.1 < 2.31.0 < 2.32.0. (Real tools use full PEP 440 version parsing to handle pre-releases and epochs, but this is the essence.)
Scanning a dependency list (tested)¶
Scanning a dependency list in Vulnerability Scanning — what it is and when to use it.
Put it together: check every requirement against a set of advisories:
def scan(requirements: dict, advisories: dict) -> list[str]:
findings = []
for pkg, version in requirements.items():
if pkg in advisories and is_vulnerable(version, advisories[pkg]):
findings.append(f"{pkg} {version} < {advisories[pkg]} (vulnerable)")
return findings
requirements = {"requests": "2.25.1", "flask": "3.0.0", "urllib3": "1.26.0"}
advisories = {"requests": "2.31.0", "urllib3": "1.26.5"}
for finding in scan(requirements, advisories):
print(finding)
Output:
Two of three dependencies are flagged; flask isn't in the advisory list so it passes. This is exactly what real scanners do — just with a comprehensive, continuously-updated advisory database instead of our tiny dict.
Real tools (use these)¶
pip-audit, safety, and Dependabot instead of hand-rolled checks.
You don't maintain the advisory database yourself — tools pull from official vulnerability feeds:
| Tool | Scans | Notes |
|---|---|---|
| pip-audit | Python deps | Official-ish; uses the PyPI advisory database |
| safety | Python deps | Checks installed packages against a CVE DB |
| Dependabot (GitHub) | Deps | Auto-opens PRs to bump vulnerable deps |
| Snyk, Trivy | Deps + containers | Broader, commercial/OSS |
| bandit | Your code (SAST) | Finds insecure patterns in your Python |
Tools follow documented usage
pip-audit, bandit, etc. aren't installed here (the scanning logic above is run-verified). These tools maintain up-to-date advisory data and understand full version semantics — always use them rather than hand-rolling, so you catch newly-disclosed CVEs.
SAST: scanning your own code¶
Static analysis (bandit, semgrep) to find insecure patterns you wrote.
Beyond dependencies, static application security testing (SAST) finds insecure patterns in your code — using the AST techniques from Static Analysis Engines. bandit flags things like:
- Hardcoded passwords/secrets (Secrets Management).
- Use of
eval/execon untrusted input. - Insecure defaults (e.g.
yaml.loadwithoutSafeLoader,shell=Truein subprocess). - Weak crypto (MD5 for passwords).
Integrate into CI¶
Fail the build on known vulnerabilities so they never ship.
The whole point is automatic, continuous scanning — catch a newly-disclosed CVE the day it lands, not months later. Add scanning to your CI/CD pipeline (like the test gate this very site uses):
# In a GitHub Actions job
- run: pip install pip-audit
- run: pip-audit # fails the build if a vulnerable dep is found
Update is the fix
Scanning finds problems; the fix is almost always upgrade the dependency to the patched version. Keep dependencies current (Dependabot automates this), and don't let a scan finding sit — an unpatched known CVE is low-hanging fruit for attackers.
Practice exercises¶
- Extend
is_vulnerableto handle a version range (vulnerable between X and Y, fixed after Y). - Read a real
requirements.txt, parsepkg==versionlines, and runscanagainst a small advisory dict. - Explain why comparing version tuples is correct but comparing version strings is buggy (
"2.9" > "2.10"). - List three insecure code patterns a SAST tool like bandit would flag.
- Add a
pip-auditstep to a CI workflow and describe what should happen when it finds a vulnerability.
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.