Private
Public Access
0
0
Files
manual_slop/scripts/audit_license_cve.py
T
ed 20fa355838 chore(deps): tilde-pin all deps; delete requirements.txt
Every direct dep in pyproject.toml now has a ~X.Y.Z bound
(patch-only). The 7 unconstrained deps (imgui-bundle,
anthropic, google-genai, openai, fastapi, mcp, uvicorn,
plus tomli-w) get explicit tilde bounds discovered from
uv.lock. The 6 >=X.Y.Z deps are normalized to tilde-style
(pinned to the current lock version).

The local-rag optional dep (sentence-transformers) is also
tilde-pinned.

requirements.txt is deleted (was redundant with uv.lock;
the uv project uses uv.lock as the canonical lock file,
which is regenerated locally and gitignored per project
policy at .gitignore:9).

Re-running the audit confirms 0 PIN_VIOLATION (was 7). The
final.md report records the post-cleanup state.

Also adds --report-name CLI flag to the audit script
(default 'initial') so the script can write either
initial.md (Phase 1) or final.md (Phase 2) into the same
report directory.
2026-06-07 15:15:30 -04:00

261 lines
9.6 KiB
Python

"""Third-party license + CVE + version-pin audit tool.
Audits the project's dependencies (pyproject.toml + uv.lock transitive
tree) for license compliance, known CVEs (via pip-audit), version
pinning, and SPDX source-headers. See
conductor/tracks/license_cve_audit_20260607/spec.md.
Output: line-per-violation to stdout (parseable) + a markdown report
under docs/reports/license_cve_audit/<date>/. The --strict flag
turns the script into a CI gate (exits non-zero on new violations
versus the baseline).
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
import tomllib
from dataclasses import dataclass, field
from importlib import metadata
from pathlib import Path
from typing import Literal
ALLOW_LICENSES: frozenset[str] = frozenset({
"MIT", "MIT-0",
"BSD", "BSD-2-Clause", "BSD-3-Clause", "0BSD",
"Apache", "Apache-2.0", "Apache 2.0", "Apache-2.0 WITH LLVM-exception",
"ISC", "ISC-License",
"Unlicense", "Unlicense-2.0",
"Zlib", "zlib-acknowledgement",
"Python-2.0", "PSF-2.0", "PSF", "CNRI-Python",
"LGPL", "LGPL-2.0", "LGPL-2.1", "LGPL-3.0", "LGPL-2.0-or-later",
"LGPL-2.1-or-later", "LGPL-3.0-or-later",
"MPL", "MPL-1.1", "MPL-2.0",
"CC0", "CC0-1.0", "WTFPL",
"Anti-996", "Anti-996-License",
"Hippocratic", "Hippocratic-2.1",
})
BLOCK_LICENSES: frozenset[str] = frozenset({
"GPL", "GPL-1.0", "GPL-2.0", "GPL-3.0",
"GPL-2.0-or-later", "GPL-3.0-or-later",
"AGPL", "AGPL-1.0", "AGPL-3.0",
"AGPL-3.0-or-later",
"SSPL", "SSPL-1.0", "Server Side Public License",
"BUSL", "BUSL-1.1",
"BSL", "BSL-1.1",
"Commons-Clause",
"Elastic", "Elastic-2.0",
})
Result = Literal["allow", "block"]
def classify_license(license_str: str | None) -> Result:
"""Classify a license string. Returns 'allow' or 'block'.
Decision rule:
- None or empty string -> 'block' (no metadata = violation)
- In BLOCK_LICENSES -> 'block'
- In ALLOW_LICENSES -> 'allow'
- Anything else (unknown / unparseable / unclassified) -> 'block'
Never auto-passes; unknown licenses are flagged for manual review.
"""
if not license_str:
return "block"
normalized = license_str.strip()
if normalized in BLOCK_LICENSES:
return "block"
if normalized in ALLOW_LICENSES:
return "allow"
return "block"
@dataclass
class Violation:
kind: Literal["license", "cve", "pin", "spdx"]
target: str
detail: str
def format_stdout(self) -> str:
return f"{self.kind.upper()}_VIOLATION target={self.target} detail={self.detail!r}"
def check_pins(pyproject_path: Path) -> list[Violation]:
"""Parse pyproject.toml and flag any dep without a version specifier."""
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
violations: list[Violation] = []
for dep in data.get("project", {}).get("dependencies", []):
name = re.split(r"[<>=!~;\[ ]", dep, maxsplit=1)[0].strip()
has_specifier = any(op in dep for op in ("<", ">", "=", "~", "!"))
if not has_specifier:
violations.append(Violation(kind="pin", target=name, detail="no version specifier in pyproject.toml"))
return violations
SPDX_PATTERN = re.compile(r"SPDX-License-Identifier:\s*(\S+)", re.IGNORECASE)
def check_source_headers(src_dir: Path) -> list[Violation]:
"""Walk src_dir for .py files; flag any with a non-permissive SPDX."""
violations: list[Violation] = []
for py_file in src_dir.rglob("*.py"):
try:
text = py_file.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
head = "\n".join(text.splitlines()[:20])
m = SPDX_PATTERN.search(head)
if m and classify_license(m.group(1)) == "block":
violations.append(Violation(
kind="spdx",
target=str(py_file),
detail=f"license={m.group(1)!r}",
))
return violations
def check_licenses() -> list[Violation]:
"""Check each installed distribution's license against the policy.
Iterates importlib.metadata.distributions(); for each, reads the
License (or License-Expression) metadata and classifies it. If
classify_license returns 'block', the dep is a violation.
"""
violations: list[Violation] = []
for dist in metadata.distributions():
name = dist.metadata["Name"]
license_str = dist.metadata.get("License") or dist.metadata.get("License-Expression")
if classify_license(license_str) == "block":
if not license_str:
detail = "no license metadata"
else:
detail = f"license={license_str!r}"
violations.append(Violation(kind="license", target=name, detail=detail))
return violations
import shutil
def check_cves() -> list[Violation]:
"""Run pip-audit as a subprocess; parse JSON output for CVEs.
If pip-audit is not installed, this is a no-op (returns []). The script
logs a warning so the user knows the CVE check was skipped.
"""
if shutil.which("pip-audit") is None:
print("WARNING: pip-audit not installed; CVE check skipped. Install via 'uv tool install pip-audit'.", file=sys.stderr)
return []
try:
result = subprocess.run(
["pip-audit", "--format=json", "--strict"],
capture_output=True, text=True, timeout=120,
)
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
print(f"WARNING: pip-audit failed: {e}", file=sys.stderr)
return []
if result.returncode != 0 and not result.stdout.strip():
print(f"WARNING: pip-audit returned non-zero with no output: {result.stderr}", file=sys.stderr)
return []
try:
data = json.loads(result.stdout)
except json.JSONDecodeError:
return []
violations: list[Violation] = []
for dep in data.get("dependencies", []):
name = dep.get("name", "<unknown>")
for vuln in dep.get("vulns", []):
cve_id = vuln.get("id", "<unknown>")
fix = ", ".join(vuln.get("fix_versions", []) or ["<unknown>"])
severity = vuln.get("severity", "unknown")
violations.append(Violation(
kind="cve", target=name,
detail=f"cve_id={cve_id} severity={severity} fix_versions={fix!r}",
))
return violations
def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="License + CVE + pin audit for third-party dependencies.")
parser.add_argument("--src", default="src", help="Source dir to scan for SPDX headers")
parser.add_argument("--scripts", default="scripts", help="Scripts dir to scan for SPDX headers")
parser.add_argument("--pyproject", default="pyproject.toml", help="Path to pyproject.toml")
parser.add_argument("--report-dir", default="docs/reports/license_cve_audit", help="Report output dir")
parser.add_argument("--date", default=None, help="ISO date for the report (default: today)")
parser.add_argument("--strict", action="store_true", help="Exit non-zero if violations > baseline")
parser.add_argument("--dump-baseline", action="store_true", help="Write current violations as the new baseline")
parser.add_argument("--report-name", default="initial", help="Report filename (default: 'initial'; use 'final' for post-cleanup)")
args = parser.parse_args()
violations: list[Violation] = []
violations.extend(check_licenses())
violations.extend(check_cves())
violations.extend(check_pins(Path(args.pyproject)))
src_dir = Path(args.src)
if src_dir.exists():
violations.extend(check_source_headers(src_dir))
scripts_dir = Path(args.scripts)
if scripts_dir.exists():
violations.extend(check_source_headers(scripts_dir))
for v in violations:
print(v.format_stdout())
from datetime import date
date_str = args.date or date.today().isoformat()
report_dir = Path(args.report_dir) / date_str
report_dir.mkdir(parents=True, exist_ok=True)
report_path = report_dir / f"{args.report_name}.md"
_write_report(violations, report_path, args)
if args.strict:
baseline_path = Path(args.report_dir).parent / "scripts" / "audit_license_cve.baseline.json"
if baseline_path.exists():
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
baseline_n = len(baseline.get("baseline_violations", []))
if len(violations) > baseline_n:
print(f"STRICT FAIL: {len(violations)} violations > {baseline_n} baseline", file=sys.stderr)
return 1
if args.dump_baseline:
baseline_path = Path(args.report_dir).parent / "scripts" / "audit_license_cve.baseline.json"
baseline_path.parent.mkdir(parents=True, exist_ok=True)
baseline_path.write_text(json.dumps({
"schema_version": 1,
"baseline_violations": [v.format_stdout() for v in violations],
"baseline_date": date_str,
"notes": "Run scripts/audit_license_cve.py --dump-baseline to regenerate.",
}, indent=2), encoding="utf-8")
print(f"Wrote {baseline_path}")
return 0
def _write_report(violations: list[Violation], path: Path, args) -> None:
by_kind: dict[str, list[Violation]] = {"license": [], "cve": [], "pin": [], "spdx": []}
for v in violations:
by_kind.setdefault(v.kind, []).append(v)
lines: list[str] = [
f"# License & CVE Audit - {args.date or 'today'}",
"",
"## Top-level summary",
"",
f"- License violations: {len(by_kind['license'])}",
f"- CVEs found: {len(by_kind['cve'])}",
f"- Pinning issues: {len(by_kind['pin'])}",
f"- SPDX violations in src/ or scripts/: {len(by_kind['spdx'])}",
"",
"## Notes",
"",
"- No `LICENSE` file in repo root - informational, not a violation. The project's own license posture is the user's call (currently all rights reserved).",
"- No source-file `SPDX-License-Identifier` headers - informational, not a violation. The project's own copyright headers are the user's call.",
"- If pip-audit is not installed, the CVE check is skipped. Install via `uv tool install pip-audit` to enable.",
"",
"## Per-violation table",
"",
"| Type | Target | Detail |",
"|------|--------|--------|",
]
for kind in ("license", "cve", "pin", "spdx"):
for v in sorted(by_kind[kind], key=lambda x: x.target):
lines.append(f"| {v.kind} | `{v.target}` | {v.detail} |")
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Wrote {path}")
if __name__ == "__main__":
sys.exit(main())