-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscan_dependencies.py
More file actions
233 lines (195 loc) Β· 7.68 KB
/
scan_dependencies.py
File metadata and controls
233 lines (195 loc) Β· 7.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
#!/usr/bin/env python3
"""
Scan Python dependencies for known vulnerabilities.
Usage:
python scan_dependencies.py [--requirements FILE] [--verbose]
"""
import sys
from pathlib import Path
# Ensure codomyrmex is in path
try:
import codomyrmex
except ImportError:
project_root = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(project_root / "src"))
import argparse
import json
import re
import subprocess
def find_requirements_files(path: str = ".") -> list:
"""Find requirements files in the project."""
found = []
root = Path(path)
for f in root.glob("requirements*.txt"):
found.append(f)
pyproject = root / "pyproject.toml"
if pyproject.exists():
found.append(pyproject)
return found
def parse_requirements(file_path: Path) -> list:
"""Parse requirements file and extract package names with versions."""
packages = []
if file_path.suffix == ".txt":
with open(file_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and not line.startswith("-"):
# Extract package name and version
match = re.match(r"^([a-zA-Z0-9_-]+)([<>=!~]+.*)?", line)
if match:
packages.append(
{
"name": match.group(1),
"version_spec": match.group(2) or "",
"line": line,
}
)
elif file_path.name == "pyproject.toml":
with open(file_path) as f:
content = f.read()
# Simple extraction of dependencies
dep_match = re.search(r"dependencies\s*=\s*\[(.*?)\]", content, re.DOTALL)
if dep_match:
deps = dep_match.group(1)
for line in deps.split("\n"):
line = line.strip().strip('",')
if line:
match = re.match(r"^([a-zA-Z0-9_-]+)", line)
if match:
packages.append(
{
"name": match.group(1),
"version_spec": "",
"line": line,
}
)
return packages
def check_pip_audit() -> bool:
"""Check if pip-audit is available."""
try:
result = subprocess.run(["pip-audit", "--version"], capture_output=True)
return result.returncode == 0
except FileNotFoundError:
return False
def run_pip_audit(requirements_file: Path) -> dict:
"""Run pip-audit on requirements file."""
try:
result = subprocess.run(
["pip-audit", "-r", str(requirements_file), "--format", "json"],
capture_output=True,
text=True,
timeout=120,
)
if result.stdout:
return json.loads(result.stdout)
return {"dependencies": [], "vulnerabilities": []}
except Exception as e:
return {"error": str(e)}
def check_known_vulnerabilities(packages: list) -> list:
"""Check packages against a list of known vulnerable packages."""
# Common packages with known vulnerabilities (simplified example)
KNOWN_VULNS = {
"urllib3": {"below": "1.26.5", "cve": "CVE-2021-33503", "severity": "HIGH"},
"requests": {"below": "2.25.0", "cve": "CVE-2018-18074", "severity": "MEDIUM"},
"pyyaml": {"below": "5.4", "cve": "CVE-2020-14343", "severity": "CRITICAL"},
"flask": {"below": "2.0.0", "cve": "CVE-2018-1000656", "severity": "HIGH"},
"django": {"below": "3.2.4", "cve": "CVE-2021-33203", "severity": "MEDIUM"},
"pillow": {"below": "8.3.2", "cve": "CVE-2021-34552", "severity": "HIGH"},
"cryptography": {
"below": "3.3.2",
"cve": "CVE-2020-36242",
"severity": "MEDIUM",
},
"jinja2": {"below": "2.11.3", "cve": "CVE-2020-28493", "severity": "HIGH"},
}
findings = []
for pkg in packages:
name_lower = pkg["name"].lower()
if name_lower in KNOWN_VULNS:
vuln = KNOWN_VULNS[name_lower]
findings.append(
{
"package": pkg["name"],
"version_spec": pkg["version_spec"],
"cve": vuln["cve"],
"severity": vuln["severity"],
"fix": f"Update to version >= {vuln['below']}",
"note": "Version check requires manual verification",
}
)
return findings
def main():
parser = argparse.ArgumentParser(
description="Scan dependencies for vulnerabilities"
)
parser.add_argument(
"--requirements", "-r", default=None, help="Requirements file to scan"
)
parser.add_argument(
"--verbose", "-v", action="store_true", help="Show all packages"
)
parser.add_argument(
"--use-pip-audit", action="store_true", help="Use pip-audit if available"
)
args = parser.parse_args()
print("π Dependency Security Scanner\n")
# Find requirements files
if args.requirements:
req_files = [Path(args.requirements)]
else:
req_files = find_requirements_files(".")
if not req_files:
print("β No requirements files found")
print(" Looking for: requirements.txt, pyproject.toml")
return 1
print(f"π Found {len(req_files)} requirements file(s):\n")
total_vulns = 0
for req_file in req_files:
print(f"π {req_file}")
packages = parse_requirements(req_file)
print(f" Packages: {len(packages)}")
if args.verbose:
for pkg in packages[:10]:
print(f" - {pkg['name']}{pkg['version_spec']}")
if len(packages) > 10:
print(f" ... and {len(packages) - 10} more")
# Check with pip-audit if requested and available
if args.use_pip_audit and check_pip_audit():
print(" Running pip-audit...")
audit_result = run_pip_audit(req_file)
if "vulnerabilities" in audit_result:
vulns = audit_result["vulnerabilities"]
if vulns:
print(f"\n β οΈ Found {len(vulns)} vulnerabilities:")
for v in vulns:
print(
f" β’ {v.get('name', 'unknown')}: {v.get('id', 'N/A')}"
)
total_vulns += len(vulns)
else:
# Use built-in check
findings = check_known_vulnerabilities(packages)
if findings:
print(f"\n β οΈ Potential issues ({len(findings)}):")
for f in findings:
severity_icon = {
"CRITICAL": "π΄",
"HIGH": "π ",
"MEDIUM": "π‘",
}.get(f["severity"], "βͺ")
print(
f" {severity_icon} {f['package']}: {f['cve']} ({f['severity']})"
)
print(f" β {f['fix']}")
total_vulns += len(findings)
print()
# Summary
if total_vulns == 0:
print("β
No known vulnerabilities detected")
print(" Note: Run with --use-pip-audit for comprehensive scanning")
else:
print(f"β οΈ Total potential issues: {total_vulns}")
print(" Review and update affected packages")
return 0 if total_vulns == 0 else 1
if __name__ == "__main__":
sys.exit(main())