-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
pip-audit-bulk
executable file
·104 lines (84 loc) · 3.21 KB
/
pip-audit-bulk
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
#!/usr/bin/env python
# pip-audit-bulk: run pip-audit in bulk, saving (reduced) reports in JSON
import collections
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path
# This entire script is path-sensitive.
_HERE = Path(__file__).resolve().parent
_REQUIREMENTS = _HERE / "requirements"
assert _REQUIREMENTS.is_dir(), "missing requirements to audit (wrong dir?)"
_OUT = _HERE / "audits"
_OUT.mkdir(exist_ok=True)
_FAILURES = _HERE / "failures"
_FAILURES.unlink(missing_ok=True)
_SKIPPED = _HERE / "skipped"
_SKIPPED.unlink(missing_ok=True)
_SUMMARY = Path(os.environ["GITHUB_STEP_SUMMARY"])
# {(dependency, CVE): [list of formula]}
_SUMMARY_RESULTS = collections.defaultdict(list)
assert shutil.which("osv-scanner"), "missing osv-scanner"
# First pass: actually run the audits.
for req in sorted(_REQUIREMENTS.glob("*.txt")):
print(f"[+] auditing: {req.name}", file=sys.stderr)
result = subprocess.run(
[
"osv-scanner",
"--lockfile",
f"requirements.txt:{req}",
"--format=json",
"--config=osv-scanner-config.toml",
],
capture_output=True,
text=True,
)
# If we don't see anything on stdout, pip-audit probably failed.
# Log it as an error.
if not result.stdout:
print(f"\t>:( pip-audit failed: {result.stderr}")
with _FAILURES.open(mode="at") as failures:
print(req.name, file=failures)
continue
# Reduce the audit to only dependencies that include vulnerabilities
audit = json.loads(result.stdout)
results = audit["results"]
# If we're left with anything, dump it as a result.
out = _OUT / req.with_suffix(".audit.json").name
if results:
assert len(results) == 1
vulnerable_packages = results[0]["packages"]
vuln_count = 0
for p in vulnerable_packages:
vuln_count += len(p["groups"])
for v in p["groups"]:
_SUMMARY_RESULTS[(p["package"]["name"], v["ids"][0])].append(
req.name.rsplit("-", 1)[0]
)
print(
f"\t:-( found {vuln_count} vulnerabilities in {req.name}",
file=sys.stderr,
)
out.write_text(json.dumps(vulnerable_packages, indent=2))
else:
out.unlink(missing_ok=True)
print(f"\t:-) no vulnerabilities found in {req.name}", file=sys.stderr)
# Second pass: clean up any audits that don't have corresponding
# requirement files, from a previous run of this script.
# This is nasty, and should probably go somewhere else.
for audit in _OUT.glob("*.json"):
req = _REQUIREMENTS / audit.with_suffix("").with_suffix(".txt").name
if not req.is_file():
print(f":-O {audit.name} orphaned ({req.name}), deleting")
audit.unlink()
render = """Identified the following vulnerabilities:
| Dependency | Vulnerability | Formula |
| ---------- | ------------- | ------- |
"""
for (dep, vuln), formulas in sorted(_SUMMARY_RESULTS.items()):
formulas_rendered = ", ".join(f"`{f}`" for f in formulas)
render += f"| `{dep}` | [`{vuln}`](https://osv.dev/vulnerability/{vuln}) | {formulas_rendered} |\n"
with _SUMMARY.open(mode="a") as io:
print(render, file=io)