Build a Dependency Vulnerability Scanner with Python
Build a Dependency Vulnerability Scanner with Python
Your requirements.txt looks clean, but one of those dependencies might be a ticking time bomb waiting to expose your users to a data breach. You don’t need to wait for a security audit to find out—you can build your own lightweight vulnerability scanner in Python today and integrate it directly into your workflow.
Security isn’t just about writing secure code; it’s about knowing what’s running in your environment. With thousands of Python packages available, the odds that you’re using a library with a known CVE (Common Vulnerabilities and Exposures) are high. Instead of relying solely on third-party tools like pip-audit or safety (which are excellent, but sometimes opaque), building your own scanner gives you full control over how vulnerabilities are detected, reported, and acted upon.
Let’s build a practical, working dependency vulnerability scanner from scratch.
Why Build Your Own Scanner?
Existing tools like pip-audit [13], safety [10], and PySentry [4] are powerful, but they come with limitations:
- They may not support your specific output format (e.g., custom JSON for CI).
- They might not integrate cleanly with your private PyPI registry.
- You can’t easily tweak the logic to match your team’s risk tolerance.
Building your own scanner lets you:
- Query the NVD (National Vulnerability Database) API directly.
- Parse
requirements.txt,pyproject.toml, orpoetry.lockfiles flexibly. - Generate reports in any format you need (Markdown, JSON, SARIF).
- Fail your CI pipeline automatically when critical CVEs are found.
Plus, it’s a great learning exercise in cybersecurity, API integration, and Python parsing.
Step 1: Set Up Your Environment
Before writing code, prepare a clean virtual environment to avoid false positives from global packages:
python3 -m venv scanner-env
source scanner-env/bin/activate
pip install requests
We’ll use the requests library to query the NVD API. No heavy dependencies—just pure Python.
Step 2: Parse Dependency Files
Your scanner needs to read your project’s dependency manifest. We’ll start with requirements.txt, the most common format.
import re
from typing import List, Dict
def parse_requirements(file_path: str) -> List[Dict[str, str]]:
"""Parse requirements.txt and return list of (package, version) dicts."""
deps = []
with open(file_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#') or line.startswith('-'):
continue
# Match package name and optional version specifier
match = re.match(r'^([a-zA-Z0-9_-]+)([=<>!~].*)?$', line)
if match:
package = match.group(1).lower()
version = match.group(2) or ''
deps.append({'package': package, 'version': version})
return deps
This function handles comments, flags like -e, and version specifiers like ==1.2.3 or >=1.0.
Step 3: Query the NVD API for CVEs
The NVD API is the gold standard for vulnerability data. We’ll query it by product name and version.
import requests
from requests.exceptions import RequestException
NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
def get_vulnerabilities(package: str, version: str) -> List[Dict]:
"""Fetch CVEs for a given package and version from NVD API."""
params = {
'productName': package,
'version': version.strip('=<>!~') if version else None
}
try:
response = requests.get(NVD_API_URL, params=params, timeout=10)
if response.status_code != 200:
print(f"⚠️ Failed to fetch CVEs for {package}: {response.status_code}")
return []
data = response.json()
return data.get('vulnerabilities', [])
except RequestException as e:
print(f"⚠️ Network error for {package}: {e}")
return []
Note: The NVD API requires no authentication for basic queries, but rate limits apply. For production use, consider caching results or using an API key.
Step 4: Build the Scanner Class
Now, let’s assemble everything into a reusable scanner.
class DependencyScanner:
def __init__(self, requirements_file: str):
self.deps = parse_requirements(requirements_file)
self.vulnerabilities = []
def scan(self) -> List[Dict]:
"""Scan all dependencies and return list of vulnerabilities."""
for dep in self.deps:
package = dep['package']
version = dep['version']
cves = get_vulnerabilities(package, version)
for vuln in cves:
cve_data = vuln.get('cve', {})
id = cve_data.get('id')
severity = 'Unknown'
description = cve_data.get('descriptions', [{}])[0].get('value', '')[:150]
# Extract severity from metrics if available
metrics = cve_data.get('metrics', {})
if 'cvssMetricV31' in metrics:
severity = metrics['cvssMetricV31'][0].get('cvssData', {}).get('severity', 'Unknown')
elif 'cvssMetricV30' in metrics:
severity = metrics['cvssMetricV30'][0].get('cvssData', {}).get('severity', 'Unknown')
self.vulnerabilities.append({
'package': package,
'version': version,
'cve_id': id,
'severity': severity,
'description': description
})
return self.vulnerabilities
def report_markdown(self) -> str:
"""Generate a Markdown report of vulnerabilities."""
report = "# 🚨 Dependency Vulnerability Report\n\n"
if not self.vulnerabilities:
report += "✅ No vulnerabilities found!\n"
return report
report += f"Found **{len(self.vulnerabilities)}** vulnerabilities:\n\n"
for v in self.vulnerabilities:
report += f"- **{v['package']}** ({v['version']}): {v['cve_id']} ({v['severity']})\n"
report += f" > {v['description']}\n\n"
return report
Step 5: Run the Scanner
Create a main.py to execute the scan:
if __name__ == "__main__":
scanner = DependencyScanner("requirements.txt")
vulns = scanner.scan()
print(scanner.report_markdown())
Run it:
python main.py
You’ll get a clean Markdown report listing all CVEs, severities, and descriptions.
Integrating with CI/CD
To make this actionable today, integrate the scanner into your CI pipeline. Export JSON output and fail the build if any CVE has a score above 7.0:
python main.py --format json > vulnerabilities.json
# In CI: parse JSON and fail if high-severity CVEs exist
GitHub Actions, GitLab CI, and Jenkins all support this pattern [3]. Mount your project, install the scanner, run the scan, and upload the report as an artifact.
What’s Next?
You’ve built a working scanner, but you can extend it further:
- Support
pyproject.tomlandpoetry.lockparsing. - Add SARIF output for IDE integration.
- Cache NVD responses to avoid rate limits.
- Auto-fix vulnerabilities using
pip-audit --fixlogic.
The beauty of building your own tool is that you control every piece of the pipeline.
Take Action Today
Don’t wait for a security incident to audit your dependencies. Run this scanner on your project right now, generate a report, and start patching. Share your findings with your team, set up a CI job to run it automatically, and make vulnerability scanning part of your development rhythm.
Your code is only secure if your dependencies are secure. Build the scanner, integrate it, and stay ahead of the threats.
👉 Try it now: Clone the code, run it on your requirements.txt, and see what vulnerabilities you uncover. Then, drop a comment below with your findings or ask for help extending the scanner to support your framework.
If you found this helpful, consider buying me a coffee ☕ — it keeps these articles coming!
Also check out my AI tools collection: AI 次元世界 — free AI tools for developers.
Top comments (0)