Skip to content

Vulnerability Scanning Proficient

🔒 Security & DevOps
⏱️ ~3 days 📚 Prerequisites: Packaging, CI/CD

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:

True
False
False

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:

requests 2.25.1 < 2.31.0 (vulnerable)
urllib3 1.26.0 < 1.26.5 (vulnerable)

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
pip-audit                 # scan the current environment
bandit -r src/            # static security analysis of your code

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/exec on untrusted input.
  • Insecure defaults (e.g. yaml.load without SafeLoader, shell=True in 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

  1. Extend is_vulnerable to handle a version range (vulnerable between X and Y, fixed after Y).
  2. Read a real requirements.txt, parse pkg==version lines, and run scan against a small advisory dict.
  3. Explain why comparing version tuples is correct but comparing version strings is buggy ("2.9" > "2.10").
  4. List three insecure code patterns a SAST tool like bandit would flag.
  5. Add a pip-audit step 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.