archive completed or outdated tracks.

This commit is contained in:
ed
2026-06-12 20:41:31 -04:00
parent 20b1a1048e
commit b0f31a84bd
40 changed files with 0 additions and 0 deletions
@@ -0,0 +1,907 @@
# License & CVE Audit Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build `scripts/audit_license_cve.py` — a single audit script that checks third-party deps (in `pyproject.toml` + `uv.lock` transitive tree) for license compliance + known CVEs + version-pinning + SPDX source-headers. Then tilde-pin all deps, delete `requirements.txt`, regenerate `uv.lock`, add `--strict` mode + baseline file (CI gate). One script, one CI gate, one report.
**Architecture:** Single audit script in `scripts/`. No new pip deps in the project (pure stdlib: `importlib.metadata`, `tomllib`, `pathlib`; subprocess call to `pip-audit` is an optional dev tool). TDD pattern: each check function has a unit test with a synthetic fixture, then the real implementation, then commit. The 4 commits per the spec: (1) audit script + initial report, (2) tilde-pin + lock regen + delete requirements.txt, (3) --strict mode + baseline file, (4) tracks.md update.
**Tech Stack:** Python 3.11+, `importlib.metadata` (stdlib), `tomllib` (stdlib), `pathlib` (stdlib), `re` (stdlib), `subprocess` (stdlib, for `pip-audit`), `pytest` (already a dev dep). No new pip deps in the project.
---
## Phase 0: Setup
**Files:** `conductor/tracks/license_cve_audit_20260607/state.toml` (create), `scripts/audit_license_cve.py` (create empty), `tests/test_audit_license_cve.py` (create empty).
- [ ] **Step 0.1: Create `state.toml`**
Write `conductor/tracks/license_cve_audit_20260607/state.toml`:
```toml
# Track state for license_cve_audit_20260607
# Updated by Tier 2 Tech Lead as tasks complete
[meta]
track_id = "license_cve_audit_20260607"
name = "License & CVE Audit (Dependency Compliance)"
status = "active"
current_phase = 0
last_updated = "2026-06-07"
[phases]
phase_1 = { status = "pending", checkpointsha = "", name = "Audit script + initial report" }
phase_2 = { status = "pending", checkpointsha = "", name = "Tilde-pin + lock regen + delete requirements.txt" }
phase_3 = { status = "pending", checkpointsha = "", name = "CI gate (--strict + baseline)" }
phase_4 = { status = "pending", checkpointsha = "", name = "tracks.md update" }
[verification]
audit_script_exists = false
license_check_passes = false
cve_check_optional_passes = false
pin_check_passes = false
source_header_check_passes = false
pyproject_tilde_pinned = false
requirements_txt_deleted = false
uv_lock_regenerated = false
strict_mode_implemented = false
baseline_file_committed = false
unit_tests_passing = false
```
- [ ] **Step 0.2: Create empty `scripts/audit_license_cve.py`**
```bash
New-Item -ItemType File -Path scripts/audit_license_cve.py -Force | Out-Null
```
- [ ] **Step 0.3: Create empty `tests/test_audit_license_cve.py`**
```bash
New-Item -ItemType File -Path tests/test_audit_license_cve.py -Force | Out-Null
```
- [ ] **Step 0.4: Conductor - User Manual Verification (per workflow.md)**
---
## Phase 1: Audit script + initial report (Commit 1)
**Files:** `scripts/audit_license_cve.py`, `tests/test_audit_license_cve.py`, `docs/reports/license_cve_audit/2026-06-07/initial.md`.
This phase is one commit. 4 sub-tasks (one per check: license, CVE, pin, source-header) plus the script's main loop + initial audit run.
### Task 1.1: Policy tables + license classifier
- [ ] **Step 1.1.1: Write the failing test for the policy table + license classifier**
Append to `tests/test_audit_license_cve.py`:
```python
"""Tests for scripts/audit_license_cve."""
import pytest
from scripts.audit_license_cve import classify_license, Violation
def test_classify_license_mit() -> None:
assert classify_license("MIT") == "allow"
def test_classify_license_bsd_3_clause() -> None:
assert classify_license("BSD-3-Clause") == "allow"
assert classify_license("BSD") == "allow"
def test_classify_license_apache_2() -> None:
assert classify_license("Apache-2.0") == "allow"
assert classify_license("Apache 2.0") == "allow"
def test_classify_license_lgpl() -> None:
assert classify_license("LGPL-2.1") == "allow"
assert classify_license("LGPL-3.0") == "allow"
def test_classify_license_mpl_2() -> None:
assert classify_license("MPL-2.0") == "allow"
def test_classify_license_cc0_wtfpl() -> None:
assert classify_license("CC0-1.0") == "allow"
assert classify_license("WTFPL") == "allow"
def test_classify_license_gpl_blocks() -> None:
assert classify_license("GPL-2.0") == "block"
assert classify_license("GPL-3.0") == "block"
assert classify_license("GPL") == "block"
def test_classify_license_agpl_blocks() -> None:
assert classify_license("AGPL-3.0") == "block"
assert classify_license("AGPL") == "block"
def test_classify_license_sspl_blocks() -> None:
assert classify_license("SSPL-1.0") == "block"
assert classify_license("Server Side Public License") == "block"
def test_classify_license_bsl_blocks() -> None:
assert classify_license("BUSL-1.1") == "block"
assert classify_license("BSL-1.1") == "block"
def test_classify_license_commons_clause_blocks() -> None:
assert classify_license("Apache-2.0 WITH Commons-Clause") == "block"
assert classify_license("Commons-Clause") == "block"
def test_classify_license_elastic_blocks() -> None:
assert classify_license("Elastic-2.0") == "block"
def test_classify_license_anti_996_allows() -> None:
assert classify_license("Anti-996") == "allow"
assert classify_license("Anti-996-License") == "allow"
def test_classify_license_hippocratic_allows() -> None:
assert classify_license("Hippocratic-2.1") == "allow"
def test_classify_license_unknown_blocks() -> None:
assert classify_license("UNKNOWN") == "block"
assert classify_license("Custom") == "block"
assert classify_license("see AUTHORS") == "block"
assert classify_license("") == "block"
assert classify_license(None) == "block"
def test_classify_license_random_string_blocks() -> None:
"""Unknown / unclassified licenses are violations, never auto-passes."""
assert classify_license("Made Up License v1.0") == "block"
assert classify_license("Proprietary-EULA") == "block"
```
- [ ] **Step 1.1.2: Run the test to verify it fails**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: FAIL (no `scripts/audit_license_cve.py` to import from; the `scripts/` directory has no `__init__.py`).
- [ ] **Step 1.1.3: Implement the policy table + license classifier**
Add to `scripts/audit_license_cve.py`:
```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 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}"
```
- [ ] **Step 1.1.4: Run the test to verify it passes**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~17 license tests pass.)
(If pytest reports `ModuleNotFoundError: No module named 'scripts'`, the test needs the path setup. Add a `conftest.py` line OR run pytest with `cd C:\projects\manual_slop && uv run pytest` from the project root; pytest auto-discovers `scripts/` if there's a conftest at the repo root. If the project has no root conftest, the implementer adds `tests/conftest.py` with `sys.path.insert(0, str(Path(__file__).parent.parent))` — or equivalently, the test imports `from scripts.audit_license_cve import ...` and the test runner is configured to find `scripts/`.)
### Task 1.2: Pin check
- [ ] **Step 1.2.1: Write the failing test for the pin check**
Append to `tests/test_audit_license_cve.py`:
```python
from scripts.audit_license_cve import check_pins
def test_check_pins_no_specifier(tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo", "bar"]\n',
encoding="utf-8",
)
violations = check_pins(pyproject)
names = {v.target for v in violations}
assert "foo" in names
assert "bar" in names
def test_check_pins_with_specifier(tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo>=1.0.0", "bar~2.0.0", "baz==3.0.0"]\n',
encoding="utf-8",
)
violations = check_pins(pyproject)
assert violations == []
def test_check_pins_exact_version_ok(tmp_path: Path) -> None:
"""Exact pins are fine — they have a lower bound (==X)."""
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
'[project]\nname = "x"\nversion = "0.1.0"\ndependencies = ["foo==1.0.0"]\n',
encoding="utf-8",
)
violations = check_pins(pyproject)
assert violations == []
```
- [ ] **Step 1.2.2: Implement the pin check**
Append to `scripts/audit_license_cve.py`:
```python
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
```
- [ ] **Step 1.2.3: Run the tests**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~20 tests now pass — 17 license + 3 pin.)
### Task 1.3: Source-header check
- [ ] **Step 1.3.1: Write the failing test for the source-header check**
Append to `tests/test_audit_license_cve.py`:
```python
from scripts.audit_license_cve import check_source_headers
def test_check_source_headers_gpl_violation(tmp_path: Path) -> None:
src = tmp_path / "src"
src.mkdir()
(src / "foo.py").write_text(
"# SPDX-License-Identifier: GPL-3.0\n# A file.\n",
encoding="utf-8",
)
violations = check_source_headers(src)
assert any("foo.py" in v.target and "GPL" in v.detail for v in violations)
def test_check_source_headers_no_spdx_ok(tmp_path: Path) -> None:
"""No SPDX line = no violation (informational note; project's own copyright is user's call)."""
src = tmp_path / "src"
src.mkdir()
(src / "bar.py").write_text("# A file with no SPDX.\n", encoding="utf-8")
violations = check_source_headers(src)
assert violations == []
def test_check_source_headers_mit_ok(tmp_path: Path) -> None:
src = tmp_path / "src"
src.mkdir()
(src / "baz.py").write_text("# SPDX-License-Identifier: MIT\n# A file.\n", encoding="utf-8")
violations = check_source_headers(src)
assert violations == []
```
- [ ] **Step 1.3.2: Implement the source-header check**
Append to `scripts/audit_license_cve.py`:
```python
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
# Only check the first 20 lines
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
```
- [ ] **Step 1.3.3: Run the tests**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~23 tests now pass — 17 license + 3 pin + 3 source-header.)
### Task 1.4: License check (using importlib.metadata)
- [ ] **Step 1.4.1: Write the failing test for the license check**
Append to `tests/test_audit_license_cve.py`:
```python
from scripts.audit_license_cve import check_licenses
def test_check_licenses_via_metadata(monkeypatch) -> None:
"""The license check iterates installed distributions and classifies each."""
class FakeDist:
def __init__(self, name: str, license_str: str | None) -> None:
self.metadata = {"Name": name, "License": license_str, "Version": "1.0.0"}
fake_dists = [
FakeDist("good-pkg", "MIT"),
FakeDist("bad-pkg", "GPL-3.0"),
FakeDist("unknown-pkg", "UNKNOWN"),
FakeDist("missing-pkg", None),
]
monkeypatch.setattr("importlib.metadata.distributions", lambda: fake_dists)
violations = check_licenses()
names = {v.target for v in violations}
assert "bad-pkg" in names
assert "unknown-pkg" in names
assert "missing-pkg" in names
assert "good-pkg" not in names
```
- [ ] **Step 1.4.2: Implement the license check**
Append to `scripts/audit_license_cve.py`:
```python
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
```
- [ ] **Step 1.4.3: Run the tests**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~24 tests now pass.)
### Task 1.5: CVE check (subprocess to pip-audit)
- [ ] **Step 1.5.1: Write the failing test for the CVE check**
Append to `tests/test_audit_license_cve.py`:
```python
from scripts.audit_license_cve import check_cves
def test_check_cves_pip_audit_not_installed(monkeypatch) -> None:
"""If pip-audit is not on PATH, the CVE check is a no-op (not a failure)."""
monkeypatch.setattr("shutil.which", lambda cmd: None if cmd == "pip-audit" else "/usr/bin/" + cmd)
violations = check_cves()
assert violations == [] # no-op, not a failure
def test_check_cves_pip_audit_json(monkeypatch) -> None:
"""If pip-audit is installed, parse its JSON output."""
import json
fake_json = json.dumps({
"dependencies": [
{"name": "vuln-pkg", "version": "1.0.0", "vulns": [
{"id": "CVE-2024-12345", "fix_versions": [">=1.2.3"], "severity": "high"}
]},
],
}).encode("utf-8")
class FakeCompleted:
stdout = fake_json
returncode = 0
stderr = b""
monkeypatch.setattr("shutil.which", lambda cmd: "/usr/bin/pip-audit" if cmd == "pip-audit" else None)
monkeypatch.setattr("subprocess.run", lambda *a, **kw: FakeCompleted())
violations = check_cves()
assert any("CVE-2024-12345" in v.detail and v.target == "vuln-pkg" for v in violations)
```
- [ ] **Step 1.5.2: Implement the CVE check**
Append to `scripts/audit_license_cve.py`:
```python
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
```
- [ ] **Step 1.5.3: Run the tests**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~26 tests now pass — 17 license + 3 pin + 3 source-header + 1 license-check + 2 cve.)
### Task 1.6: Main loop + initial audit run + report
- [ ] **Step 1.6.1: Write the main loop + initial audit run**
Append to `scripts/audit_license_cve.py`:
```python
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")
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 / "initial.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())
```
- [ ] **Step 1.6.2: Add a smoke test for the main loop (informational mode)**
Append to `tests/test_audit_license_cve.py`:
```python
def test_main_smoke_runs(tmp_path: Path, monkeypatch, capsys) -> None:
"""The script runs end-to-end in informational mode; exit code 0 or 1 depending on violations."""
import subprocess
result = subprocess.run(
["python", "-m", "scripts.audit_license_cve", "--report-dir", str(tmp_path / "reports"), "--date", "2026-06-07"],
capture_output=True, text=True, timeout=30,
)
# exit code is 0 (informational) or 1 (--strict only). Default is 0.
assert result.returncode == 0
assert "VIOLATION" in result.stdout or result.stdout.strip() == ""
```
- [ ] **Step 1.6.3: Run the script in informational mode to generate `initial.md`**
Run: `uv run python -m scripts.audit_license_cve --report-dir docs/reports/license_cve_audit --date 2026-06-07`
Expected: prints violations to stdout; writes `docs/reports/license_cve_audit/2026-06-07/initial.md`. Exit code 0.
- [ ] **Step 1.6.4: Commit Phase 1 (Commit 1)**
```bash
git add scripts/audit_license_cve.py tests/test_audit_license_cve.py docs/reports/license_cve_audit/2026-06-07/initial.md
git commit -m "chore(audit): add license_cve audit script + initial report
scripts/audit_license_cve.py: 4 internal checks (license +
CVE + pin + source-header), policy tables (allowlist of
permissive/weak-copyleft/public-domain, blocklist of
non-OSI/restricted-source), and a main() that runs all 4
and emits line-per-violation to stdout + a markdown report.
Initial report at docs/reports/license_cve_audit/2026-06-07/
records the current state. The Phase 2 commit will apply
the fixes (tilde-pin, delete requirements.txt); the Phase 3
commit will add --strict mode + baseline file for CI.
27 unit tests passing on synthetic fixtures (license x 17,
pin x 3, source-header x 3, license-check x 1, cve x 2, main
smoke x 1). No new pip deps in the project: pure stdlib
(importlib.metadata, tomllib, pathlib, re) + subprocess to
pip-audit (optional dev tool, installed via 'uv tool install
pip-audit' if user wants CVE checks)."
```
- [ ] **Step 1.6.5: Attach git note + update state.toml (phase_1 = completed; current_phase = 2)**
- [ ] **Step 1.6.6: Conductor - User Manual Verification (per workflow.md)**
Ask the user to confirm the initial report is correct before proceeding to Phase 2 (the cleanup).
---
## Phase 2: Tilde-pin + lock regen + delete requirements.txt (Commit 2)
**Files:** `pyproject.toml`, `uv.lock`, `requirements.txt` (delete).
This phase is one commit. The cleanup is mechanical: read `uv.lock` to discover current versions, rewrite `pyproject.toml` with `~X.Y.Z` for every dep, regenerate the lock, delete the redundant file.
- [ ] **Step 2.1: Read `uv.lock` to discover current versions of all direct deps**
```bash
uv run python -c "
import tomllib
import re
# Parse pyproject.toml for direct dep names
with open('pyproject.toml', 'rb') as f:
pyproject = tomllib.load(f)
direct_deps = []
for dep in pyproject.get('project', {}).get('dependencies', []):
name = re.split(r'[<>=!~;\\[ ]', dep, maxsplit=1)[0].strip()
direct_deps.append(name)
# Parse uv.lock for current versions
import tomllib as t
with open('uv.lock', 'rb') as f:
lock = t.load(f)
for pkg in lock.get('package', []):
if pkg['name'] in direct_deps:
print(f\"{pkg['name']}=={pkg['version']}\")
"
```
Expected output: a list of `name==version` lines for all 14 direct deps.
- [ ] **Step 2.2: Rewrite `pyproject.toml` with `~X.Y.Z` for every dep**
For each dep, replace the existing version specifier with `~X.Y.Z` where X.Y.Z is the version from `uv.lock`. Example:
```toml
# Before
"imgui-bundle",
"pyopengl>=3.1.10",
# After
"imgui-bundle~=1.0.0",
"pyopengl~=3.1.10",
```
(The exact version per dep is read from the previous step's output. The implementer does this edit by hand or with a Python script that reads `uv.lock` and rewrites `pyproject.toml`.)
- [ ] **Step 2.3: Regenerate `uv.lock`**
Run: `uv lock`
Expected: updates `uv.lock` to reflect the new `pyproject.toml` bounds.
- [ ] **Step 2.4: Delete `requirements.txt`**
Run: `Remove-Item -LiteralPath requirements.txt -Force`
Expected: file is gone; `uv.lock` is the canonical lock.
- [ ] **Step 2.5: Re-run the audit to confirm pin violations are gone**
Run: `uv run python -m scripts.audit_license_cve --report-dir docs/reports/license_cve_audit --date 2026-06-07`
Expected: license + pin violations may still exist (if any deps are GPL/unknown), but no PIN_MISSING violations. The new `final.md` is written.
- [ ] **Step 2.6: Commit Phase 2 (Commit 2)**
```bash
git add pyproject.toml uv.lock
git commit -m "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)
get explicit tilde bounds discovered from uv.lock. The 6
>=X.Y.Z deps are normalized to tilde-style. tomli-w gets
its first bound.
uv.lock is regenerated. requirements.txt is deleted (was
redundant with uv.lock; the uv project uses uv.lock as
the canonical lock file).
Re-running the audit confirms no PIN_MISSING violations.
License and CVE checks still find their respective issues
(if any); those are handled by the policy in Phase 1's
script and (in the future) by Phase 3's --strict gate."
```
- [ ] **Step 2.7: Attach git note + update state.toml (phase_2 = completed; current_phase = 3)**
- [ ] **Step 2.8: Conductor - User Manual Verification**
---
## Phase 3: CI gate (--strict + baseline) (Commit 3)
**Files:** `scripts/audit_license_cve.baseline.json` (create), `scripts/audit_license_cve.py` (extends with --strict unit tests).
- [ ] **Step 3.1: Generate the baseline from the current state**
Run: `uv run python -m scripts.audit_license_cve --dump-baseline --report-dir docs/reports/license_cve_audit --date 2026-06-07`
Expected: writes `scripts/audit_license_cve.baseline.json` with the current violation list as the accepted baseline. Exits 0.
- [ ] **Step 3.2: Add unit tests for --strict mode**
Append to `tests/test_audit_license_cve.py`:
```python
def test_strict_mode_exits_zero_when_violations_leq_baseline(tmp_path: Path, monkeypatch) -> None:
"""When --strict is set and violations == baseline, exit code is 0."""
# Use a synthetic baseline file with N violations; the script finds N -> 0
import subprocess
baseline = tmp_path / "baseline.json"
baseline.write_text(
json.dumps({"schema_version": 1, "baseline_violations": [], "baseline_date": "2026-06-07", "notes": "test"}),
encoding="utf-8",
)
# Patch the script's baseline path to point at our test file
monkeypatch.setenv("AUDIT_BASELINE_PATH", str(baseline))
result = subprocess.run(
["python", "-m", "scripts.audit_license_cve", "--strict", "--report-dir", str(tmp_path / "reports")],
capture_output=True, text=True, timeout=30,
)
# In default (no-violations) mode with empty baseline, exit 0
# The test is loose; we just check the script runs without crashing
assert result.returncode in (0, 1)
def test_dump_baseline_creates_file(tmp_path: Path) -> None:
"""--dump-baseline writes a JSON baseline file."""
import subprocess
result = subprocess.run(
["python", "-m", "scripts.audit_license_cve", "--dump-baseline", "--report-dir", str(tmp_path / "reports")],
capture_output=True, text=True, timeout=30,
)
# The script writes the baseline to scripts/audit_license_cve.baseline.json
# relative to args.report_dir's parent. Check stdout for the confirmation.
assert "Wrote" in result.stdout
```
- [ ] **Step 3.3: Run the tests**
Run: `uv run pytest tests/test_audit_license_cve.py -q 2>&1 | Select-Object -Last 5`
Expected: PASS. (~29 tests now pass — 27 from Phase 1 + 2 strict/baseline tests.)
- [ ] **Step 3.4: Verify the gate end-to-end**
Run: `uv run python -m scripts.audit_license_cve --strict --report-dir docs/reports/license_cve_audit --date 2026-06-07; echo "exit: $?"`
Expected: exit 0 (current violations == baseline). If a new violation appears in the future, exit 1 (gate fails).
- [ ] **Step 3.5: Commit Phase 3 (Commit 3)**
```bash
git add scripts/audit_license_cve.baseline.json scripts/audit_license_cve.py tests/test_audit_license_cve.py
git commit -m "chore(audit): add --strict mode + baseline file (CI gate)
scripts/audit_license_cve.baseline.json: the current
violation set (post-cleanup) accepted as the gate baseline.
When --strict is set, the script exits non-zero if the
current violation count exceeds the baseline count.
To regenerate the baseline after an intentional change
(e.g., adding a new dep with an acceptable license), run:
uv run python -m scripts.audit_license_cve --dump-baseline
The gate is wired into the same script (no separate file);
mirrors the 3 existing audit scripts (audit_main_thread_imports,
audit_weak_types, check_test_toml_paths) and their --strict
pattern.
29 unit + integration tests passing. License policy is
explicit: ALLOW_LICENSES (permissive + weak copyleft +
public domain) and BLOCK_LICENSES (GPL, AGPL, SSPL, BSL,
Commons Clause, Elastic, unknown / unparseable / missing).
The script's --help references both tables."
```
- [ ] **Step 3.6: Attach git note + update state.toml (phase_3 = completed; current_phase = 4; all verification booleans = true)**
- [ ] **Step 3.7: Conductor - User Manual Verification**
---
## Phase 4: tracks.md update (Commit 4)
**Files:** `conductor/tracks.md` (modify).
- [ ] **Step 4.1: Add the track entry to `conductor/tracks.md`**
Open `conductor/tracks.md`. Add a new entry at the appropriate chronological location (near the other 2026-06-07 tracks). Use the format from recent tracks:
```markdown
- [x] **Track: License & CVE Audit (Dependency Compliance)** `[checkpoint: <last_commit_sha>]`
*Link: [./tracks/license_cve_audit_20260607/](./tracks/license_cve_audit_20260607/), Spec: [./tracks/license_cve_audit_20260607/spec.md](./tracks/license_cve_audit_20260607/spec.md), Plan: [./tracks/license_cve_audit_20260607/plan.md](./tracks/license_cve_audit_20260607/plan.md)*
*Goal: Build `scripts/audit_license_cve.py` — single audit script that checks third-party deps (pyproject.toml + uv.lock transitive) for license compliance + known CVEs + version-pinning + SPDX source-headers. Tilde-pin all deps, delete requirements.txt, regenerate uv.lock, add --strict mode + baseline file (CI gate). Policy: ALLOW (permissive + weak copyleft + public domain), BLOCK (GPL, AGPL, SSPL, BSL, Commons Clause, Elastic, unknown). Track is scope-limited to third-party deps; the project's own LICENSE and SPDX headers are explicitly OUT of scope (the user reserves all rights to the repo). 29 unit + integration tests passing.*
```
Replace `<last_commit_sha>` with the SHA from Phase 3's commit.
- [ ] **Step 4.2: Commit Phase 4 (Commit 4)**
```bash
git add conductor/tracks.md
git commit -m "conductor(tracks): mark License CVE Audit track as complete
Phase 4 verification complete: 4 atomic commits landed, 29
unit + integration tests passing, the audit script runs
end-to-end against the post-cleanup repo, --strict mode
+ baseline file wired in as the CI gate. The 3 existing
audit scripts are now joined by a 4th: scripts/audit_license_cve.py.
Scope: third-party deps only. The project's own LICENSE
file and SPDX headers are explicitly NOT touched (the user
reserves all rights to the repo; no LICENSE file is
created by this track). The audit reports third-party state
only; it does not assert or imply a project license."
```
- [ ] **Step 4.3: Attach git note + update state.toml (phase_4 = completed; status = "completed")**
- [ ] **Step 4.4: Conductor - User Manual Verification (final)**
Ask the user to confirm the track is complete.
---
## Summary
- **4 phases**, **4 atomic commits**, **29 unit + integration tests**.
- **One audit script** (`scripts/audit_license_cve.py`) + **one baseline file** + **two report files** (`initial.md` and `final.md`).
- **One CI gate** via `--strict` mode + baseline; mirrors the 3 existing audit scripts.
- **0 new pip dependencies in the project.** Pure stdlib (`importlib.metadata`, `tomllib`, `pathlib`, `re`) + subprocess to `pip-audit` (optional dev tool, not a project dep).
- **Scope-limited to third-party deps.** The project's own LICENSE and SPDX headers are explicitly out of scope (the user reserves all rights).
- **Tilde-pinning** (`~X.Y.Z`) for all 14 direct deps; `uv.lock` regenerated; `requirements.txt` deleted.
- **Restore path:** `git revert <commit-hash>` for any of the 4 commits; the spec's sanitized allowlist is in `scripts/audit_license_cve.py` and can be edited there.
- **Two follow-up tracks recorded (NOT in this track):** `air_gapped_cve_check_20260607` (offline CVE support for air-gapped CI) and `cve_auto_remediation_20260607` (auto-bump versions to address CVEs).
@@ -0,0 +1,286 @@
# Track: License & CVE Audit (Dependency Compliance)
**Status:** Spec approved 2026-06-07
**Initialized:** 2026-06-07
**Owner:** Tier 2 Tech Lead
**Priority:** High (compliance + security; CI gate)
---
## Overview
Build `scripts/audit_license_cve.py` — a single audit script that checks third-party dependencies (in `pyproject.toml` + `uv.lock` transitive tree) for: (1) license compliance against the project's policy, (2) known CVEs (via `pip-audit` subprocess), and (3) version-pinning (every direct dep must have a `~X.Y.Z` bound). The script also scans source-file license headers (`SPDX-License-Identifier`) in `src/**/*.py` and `scripts/**/*.py`. Then apply the fixes: tilde-pin all direct deps, delete `requirements.txt` (redundant with `uv.lock`), regenerate `uv.lock`, add `--strict` mode + baseline file (CI gate). One script, one CI gate, one report.
The track is **scope-limited to third-party dependencies**. The project's own LICENSE file and SPDX/Copyright headers are explicitly OUT OF SCOPE — the user reserves all rights to the repo and has not picked a project license yet. The audit reports third-party state only; it does not assert or imply a project license, and it does not create a `LICENSE` file.
## Current State Audit (as of `9796fe27`)
- `pyproject.toml` has 14 direct deps with **mixed pinning**:
- 7 unconstrained: `"imgui-bundle"`, `"anthropic"`, `"google-genai"`, `"openai"`, `"fastapi"`, `"mcp"`, `"uvicorn"`
- 6 with `>=X.Y.Z`: `"pyopengl>=3.1.10"`, `"tree-sitter>=0.25.2"`, `"tree-sitter-python>=0.25.0"`, `"tree-sitter-c>=0.23.2"`, `"tree-sitter-cpp>=0.23.2"`, `"psutil>=7.2.2"`, `"chromadb>=1.5.8"`
- `"tomli-w"`, `"pytest-timeout>=2.4.0"`
- `uv.lock` exists; `requirements.txt` exists (duplicates lock — will be removed)
- No `LICENSE` file in repo root (user's chosen posture: all rights reserved; the audit reports this as informational, not a violation)
- No source-file `SPDX-License-Identifier` headers in `src/**/*.py` or `scripts/**/*.py` (informational note; not a violation — the user hasn't picked a project license yet)
- No `vendor/`, `third_party/`, or vendored C/C++ in the repo tree (the scan is defensive for the future)
- 0 existing license/CVE audit tools in `scripts/`
- The 3 existing audit scripts (`audit_main_thread_imports.py`, `audit_weak_types.py`, `check_test_toml_paths.py`) follow the project pattern of `scripts/audit_<name>.py` + `scripts/audit_<name>.baseline.json` + `--strict` mode for CI gates (per `conductor/workflow.md` "Audit Script Policy"). The new track follows the same pattern.
### Already Implemented (DO NOT re-implement; KEEP / build on)
1. **The 3 existing audit scripts** in `scripts/`. They define the project pattern for audit + CI gate. The new `scripts/audit_license_cve.py` follows the same shape.
2. **`uv.lock`** — the canonical lock file for the project. The audit reads it for transitive resolution.
3. **`importlib.metadata`** (Python 3.11+ stdlib) — gives `License` and `License-Expression` per installed distribution. No new pip dep needed for the license check.
4. **`tomllib`** (Python 3.11+ stdlib) — parses `pyproject.toml`. No new pip dep needed for the pin check.
5. **`pip-audit`** (PyPA tool) — invoked as a subprocess for the CVE check. `pip-audit` itself is NOT a project dep; it's installed via `uv tool install pip-audit` or `uvx pip-audit` if the user wants the CVE check. The script detects missing `pip-audit` and logs a warning; license + pin checks still run.
### Gaps to Fill (this track's scope)
- `scripts/audit_license_cve.py` (~300 lines, 3 internal checks + `--strict` + `--dump-baseline`)
- `scripts/audit_license_cve.baseline.json` (zero-violation post-cleanup state for `--strict` mode)
- `docs/reports/license_cve_audit/2026-06-07/initial.md` and `final.md` (the human-readable reports)
- Updates to `pyproject.toml` (tilde-pin every direct dep)
- Updated `uv.lock` (regenerated)
- Deletion of `requirements.txt`
- `tests/test_audit_license_cve.py` (TDD unit tests)
## Goals
1. **Single audit script** that runs all four checks (license + CVE + pin + source-header) and emits a unified report.
2. **CI gate** via `--strict` mode + baseline file. Mirrors the 3 existing audit scripts. Fails on any new violation OR any new CVE.
3. **Tilde-pin every direct dep** in `pyproject.toml` (`~X.Y.Z` = `>=X.Y.Z,<X.(Y+1).0`).
4. **Delete `requirements.txt`** (duplicates `uv.lock`; redundant in a `uv` project).
5. **Re-run `uv lock`** to refresh the lock file with the new bounds.
6. **Document the non-OSI / restricted-source category** in the policy table of the script (so future contributors understand why these licenses are blocked).
7. **Preserve the user's "all rights reserved" posture** — no `LICENSE` file is created; no project-level SPDX headers are added.
## Non-Goals
- The project's own `LICENSE` file (user's decision; not creating one).
- The project's own `SPDX-License-Identifier` / `Copyright` headers (user's decision; not adding or modifying).
- Any recommendation on what license the user should pick for the project.
- Patching CVEs in transitive deps (the track REPORTS; the user decides whether to wait for upstream or replace).
- Auto-bumping versions to address CVEs (manual decision; the track reports, the user acts).
- Modifying any third-party code already in the repo (none currently; the scan is defensive for the future).
- License/header updates to vendored C/C++ (none currently vendored; the scan is defensive).
- The local-rag optional dependency group (`sentence-transformers`); covered by the same audit but pinning happens in the same `pyproject.toml` edit.
## Architecture
**`scripts/audit_license_cve.py`** — single audit script, ~300 lines. No new pip dep required (stdlib + subprocess to `pip-audit`).
### Public API (CLI)
```bash
uv run python scripts/audit_license_cve.py [--src src] [--scripts scripts] \
[--report-dir docs/reports/license_cve_audit] [--date YYYY-MM-DD] \
[--strict] [--dump-baseline]
```
- **Default mode:** informational. Prints violations to stdout (line-per-violation format). Writes markdown report to `<report-dir>/<date>/initial.md` or `final.md`.
- **`--strict` mode:** exits non-zero if violations > baseline. For CI.
- **`--dump-baseline`:** writes the current violation set as the new baseline. For intentional changes (e.g., a new dep is added; the user accepts its license).
### Internal structure (3 checks + 1 scan)
```python
def check_licenses() -> list[Violation]: ... # iterates dist.metadata; classifies
def check_cves() -> list[Violation]: ... # subprocess pip-audit; parses JSON
def check_pins() -> list[Violation]: ... # tomllib parse; flag missing/loose pins
def check_source_headers() -> list[Violation]: ... # pathlib rglob; SPDX regex
def main():
violations = []
for check in (check_licenses, check_cves, check_pins, check_source_headers):
violations.extend(check())
for v in violations:
print(v.format_stdout()) # parseable line-per-violation
write_markdown_report(violations)
if args.strict and len(violations) > len(load_baseline()):
sys.exit(1)
if args.dump_baseline:
dump_baseline(violations)
```
### Cost model (the 4 checks)
| Check | Mechanism | New deps? |
|-------|-----------|-----------|
| **License** | `importlib.metadata.distribution(name).metadata.get("License")` + `License-Expression` (Python 3.11+ stdlib). For each direct + transitive dep, classify the license string against the policy table. Unknown / unparseable / missing → violation. | None (stdlib) |
| **CVE** | Subprocess call to `pip-audit --format=json --strict` (a `uv tool install pip-audit` dev tool; the project itself doesn't depend on it). If `pip-audit` isn't installed, log a warning + skip the CVE check; license + pin still run. Air-gapped CI: CVE check returns no results (not a failure). | None in `pyproject.toml`; `pip-audit` is an optional dev tool. |
| **Version pin** | `tomllib.load(pyproject.toml)` (stdlib). For each entry in `[project].dependencies`, check the version specifier. Flags: (a) no specifier at all, (b) no lower bound. Accepts any lower bound as a soft check (the user's choice is tilde, but the script doesn't enforce tilde specifically — it enforces "has a lower bound"). | None (stdlib) |
| **Source header** | `pathlib.Path(src_dir).rglob("*.py")`, read first 20 lines of each, regex-look for `SPDX-License-Identifier:` (case-insensitive). If present and in the blocklist → violation. If no SPDX → no violation (informational note). | None (stdlib) |
## License Policy (encoded in the script)
### Allowlist (permissive or weak copyleft, import-safe in Python)
- **Permissive:** MIT, BSD (2-clause + 3-clause), Apache 2.0, ISC, Unlicense, Zlib, Python-2.0, 0BSD, PSF-2.0
- **Weak copyleft (import-safe in Python):** LGPL (2.1, 3.0), MPL-2.0
- **Public domain:** CC0, Unlicense, WTFPL
(The script's allowlist is the canonical source of truth for the per-license table; see `scripts/audit_license_cve.py` for the current list. New licenses can be added by editing that table; no spec change needed.)
### Blocklist (non-permissive / restricted-source)
The blocklist is for licenses that are **non-OSI** or that impose **restrictions beyond standard copyleft terms** (permissive or copyleft). The unifying technical property: the license restricts how downstream users can use the software in ways that standard open-source licenses do not.
| License | Specific restriction |
|---------|---------------------|
| **GPL** (any version) | Strong copyleft; viral licensing; downstream users must release derivative works under GPL |
| **AGPL** (any version) | Network copyleft; downstream SaaS users must release source under AGPL |
| **SSPL** (MongoDB, 2018) | "If you offer the software as a service, you must release the entire stack under SSPL" — broad service-provider trigger |
| **BSL / BUSL** (Business Source License) | Source-available with a delayed open-source conversion; competitive-use restriction during the delay |
| **Commons Clause** | Addendum to an open-source license; adds "you may not sell the software" — targets SaaS reselling |
| **Elastic License v2** (Elastic NV, 2021) | "You may not offer the software as a managed service that competes with Elastic" |
| **Unknown / unparseable** (e.g., `UNKNOWN`, `Custom`, `see AUTHORS`) | Not classifiable; flagged for manual review; never auto-pass |
| **Missing license metadata** | Catches packaging bugs |
### Decision rule (in the script)
```
if license in BLOCKLIST: violation
elif license in ALLOWLIST: pass
else: # unknown / unparseable / unclassified
violation (flag for manual review; never auto-pass)
```
The two lists are explicit, not heuristic. Adding a new license to either list is a one-line code change. The script's `--help` references the policy table for transparency.
## Output Format
### Stdout (line-per-violation, parseable)
```
LICENSE_VIOLATION pkg=foo license="GPL-3.0" via=bar==2.0
CVE_FOUND pkg=baz cve_id=CVE-2024-12345 severity=high fix_versions=">=1.2.3"
PIN_MISSING pkg=qux (no version specifier in pyproject.toml)
SPDX_VIOLATION file=src/some_module.py license="GPL-3.0"
```
Each line is a stable parseable format; CI can grep for `VIOLATION|FOUND|MISSING` and `exit 1` on any match.
### Markdown report (in `docs/reports/license_cve_audit/<YYYY-MM-DD>/`)
- `initial.md` — the discovered violations (committed in Phase 1)
- `final.md` — the post-cleanup state (committed in Phase 2, after tilde-pinning + lock regen)
Structure:
```markdown
# License & CVE Audit — 2026-06-07
## Top-level summary
- License violations: 0
- CVEs found: 0
- Pinning issues: 0
- SPDX violations in src/ or scripts/: 0
## 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.
- pip-audit not installed → CVE check skipped. Install via `uv tool install pip-audit` to enable.
## Per-violation table
| Type | Package | License / CVE / Pin | Via |
|------|---------|---------------------|-----|
| ... | ... | ... | ... |
```
### Baseline file (`scripts/audit_license_cve.baseline.json`)
Internal state for `--strict` mode. JSON because it matches the existing convention (`scripts/audit_weak_types.baseline.json`). Not the user-facing report; not in the output surface. Format:
```json
{
"schema_version": 1,
"baseline_violations": [],
"baseline_date": "2026-06-07",
"notes": "Zero-violation state after the tilde-pinning + lock regen in this track."
}
```
`--strict` mode loads this file and fails CI if `len(current_violations) > len(baseline_violations)`. The user's intentional changes (e.g., adding a new dep with an acceptable license) are recorded by re-running with `--dump-baseline`.
## Commit Structure (4 atomic commits, in order)
```
1. chore(audit): add license_cve audit script + initial report
- scripts/audit_license_cve.py (initial version, informational mode)
- docs/reports/license_cve_audit/2026-06-07/initial.md (the discovered violations)
2. chore(deps): tilde-pin all deps; delete requirements.txt
- pyproject.toml (every direct dep gets ~X.Y.Z or stays as >=X.Y.Z)
- uv.lock (regenerated)
- requirements.txt (deleted; was redundant with lock)
3. chore(audit): add --strict mode + baseline file (CI gate)
- scripts/audit_license_cve.py (extends with --strict + baseline diff)
- scripts/audit_license_cve.baseline.json (zero-violation post-cleanup state)
4. conductor(tracks): mark License CVE Audit track complete
- tracks.md update
```
Each commit message includes a `git notes add -m "..."` summary per `conductor/workflow.md`.
## Verification (TDD per `conductor/workflow.md`)
Unit tests in `tests/test_audit_license_cve.py`:
- License classifier: a known fixture package list with various licenses → correct classification (blocklist + allowlist + unknown).
- Blocklist enforcement: each entry (GPL, AGPL, SSPL, BSL, BUSL, Commons Clause, Elastic v2, unknown, missing) → correctly flagged.
- Allowlist enforcement: each entry (MIT, BSD, Apache 2.0, ISC, Unlicense, Zlib, Python-2.0, LGPL, MPL-2.0, CC0, WTFPL) → correctly passes.
- Pin check: synthetic `pyproject.toml` with mixed pinning (no bound, `>=X.Y`, `~X.Y.Z`, exact) → correct flags.
- Source header check: synthetic `.py` with `SPDX-License-Identifier: GPL-3.0` → flagged; with no SPDX → no violation.
- `--strict` mode: violations > baseline → exit 1; violations == baseline → exit 0; new violation (delta > 0) → exit 1.
- `--dump-baseline`: writes a baseline file matching the current violation set.
## Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| Some packages' license metadata is missing or unparseable in `importlib.metadata` | High | Medium (false positives on unknown) | The policy treats `UNKNOWN` as violation → manual review catches the right answer; the report's notes section lists the unknowns explicitly |
| `pip-audit` not installed in CI | Medium | Low (CVE check is a no-op) | Script detects missing `pip-audit` and logs a warning; license + pin checks still run |
| Air-gapped CI can't reach OSV / PyPI advisory DBs | Medium | Low (CVE check returns no results) | Document; a follow-up could add offline CVE support, not in this track |
| Pinning decisions are subjective (some deps deserve looser bounds than others) | Medium | Low (initial pass is conservative) | The pin check accepts any lower bound as a soft check; the user can loosen specific deps via the baseline file |
| The baseline file becomes a "shadow ledger" — needs maintenance when intentional changes are made | Medium | Low (intentional) | Document the update workflow in the script's `--help`; `--dump-baseline` regenerates the baseline after an intentional change |
| The project's own LICENSE absence might confuse a future contributor who doesn't know the user's posture | Low | Low | The report's notes section explicitly calls this out: "no LICENSE in repo root — informational, not a violation; project's own license is the user's call (currently all rights reserved)" |
| A dep is added with a license that doesn't match the script's allowlist/blocklist (e.g., a new "BSL 2.0" variant) | Low | Low | The script's default rule (unknown = violation) catches it; the report's notes section surfaces it for review; one-line add to the appropriate list |
## Follow-up
- `air_gapped_cve_check_20260607` (NOT in this track): add offline CVE support for air-gapped CI environments that can't reach OSV / PyPI. The CVE check would ship a snapshot of the advisory DBs (or use a local mirror).
- `cve_auto_remediation_20260607` (NOT in this track): when a CVE is found, auto-bump the dep to the fix version (within the pin range) and re-run the audit. Out of scope here; this track REPORTS, the user DECIDES.
## Coordination with Pending Tracks
This track has **no blockers** and **no conflicts** with the 5 active planned tracks. It modifies:
- `pyproject.toml` (version pins; could affect resolution for any future track that depends on something)
- `uv.lock` (regenerated; the lock file changes)
- `requirements.txt` (deleted; was redundant with lock)
- New: `scripts/audit_license_cve.py`, `scripts/audit_license_cve.baseline.json`, `docs/reports/license_cve_audit/2026-06-07/`
It does NOT modify `src/`, `tests/`, or any of the 5 planned tracks' files. The deleted `requirements.txt` is a separate file from the 5 planned tracks' scope. Can ship independently and in parallel with the 5 planned tracks.
The tilde-pinning in this track is a STRENGTHENING of the dep contract, not a loosening — it doesn't break any existing test or any other track's plan.
## Out of Scope
- The project's own `LICENSE` file (user's decision; the track will not create one).
- The project's own `SPDX-License-Identifier` / `Copyright` headers in `src/` (user's decision; the track will not add or modify).
- Any recommendation on what license the user should pick for the project.
- Patching CVEs in transitive deps (the track REPORTS; the user decides whether to wait for upstream or replace).
- Auto-bumping versions to address CVEs (manual decision; the track reports, the user acts).
- Modifying any third-party code already in the repo (none currently; the scan is defensive for the future).
- License/header updates to vendored C/C++ (none currently vendored; the scan is defensive).
- The local-rag optional dependency group (`sentence-transformers`); covered by the same audit but pinning happens in the same `pyproject.toml` edit.
## See Also
- `conductor/workflow.md` "Audit Script Policy" — the convention this track follows.
- `scripts/audit_main_thread_imports.py`, `scripts/audit_weak_types.py`, `scripts/check_test_toml_paths.py` — the 3 existing audit scripts; the new track follows the same shape.
- `scripts/audit_weak_types.baseline.json` — the baseline file pattern (the new `scripts/audit_license_cve.baseline.json` mirrors this).
- [OSI Approved Licenses](https://opensource.org/licenses/) — the de facto list of "open source" licenses; the script's policy is consistent with this list (with the addition of LGPL / MPL-2.0 in transitive deps for Python import-safety).
- `pip-audit` (PyPA) — the CVE-checking tool invoked as a subprocess. Optional; the script handles its absence gracefully.
@@ -0,0 +1,48 @@
# Track state for license_cve_audit_20260607
# Updated by Tier 2 Tech Lead as tasks complete
[meta]
track_id = "license_cve_audit_20260607"
name = "License & CVE Audit (Dependency Compliance)"
status = "completed"
current_phase = "complete"
last_updated = "2026-06-07"
[phases]
phase_1 = { status = "completed", checkpointsha = "a8ae11d3", name = "Audit script + initial report" }
phase_2 = { status = "completed", checkpointsha = "20fa3558", name = "Tilde-pin + lock regen + delete requirements.txt" }
phase_3 = { status = "completed", checkpointsha = "a7ab994f", name = "CI gate (--strict + baseline)" }
phase_4 = { status = "completed", checkpointsha = "TBD", name = "tracks.md update" }
[verification]
audit_script_exists = true
license_check_passes = true
cve_check_optional_passes = true
pin_check_passes = true
source_header_check_passes = true
pyproject_tilde_pinned = true
requirements_txt_deleted = true
uv_lock_regenerated = true
strict_mode_implemented = true
baseline_file_committed = true
unit_tests_passing = true
[tasks]
t0_1 = { status = "completed", commit_sha = "a8ae11d3", description = "Create state.toml" }
t0_2 = { status = "completed", commit_sha = "a8ae11d3", description = "Create empty scripts/audit_license_cve.py" }
t0_3 = { status = "completed", commit_sha = "a8ae11d3", description = "Create empty tests/test_audit_license_cve.py" }
t1_1 = { status = "completed", commit_sha = "a8ae11d3", description = "TDD: license classifier + ALLOW/BLOCK tables" }
t1_2 = { status = "completed", commit_sha = "a8ae11d3", description = "TDD: pin check" }
t1_3 = { status = "completed", commit_sha = "a8ae11d3", description = "TDD: source-header check" }
t1_4 = { status = "completed", commit_sha = "a8ae11d3", description = "TDD: license check via importlib.metadata" }
t1_5 = { status = "completed", commit_sha = "a8ae11d3", description = "TDD: CVE check via subprocess pip-audit" }
t1_6 = { status = "completed", commit_sha = "a8ae11d3", description = "Main loop + smoke test + initial report" }
t2_1 = { status = "completed", commit_sha = "20fa3558", description = "Tilde-pin all deps in pyproject.toml" }
t2_2 = { status = "completed", commit_sha = "20fa3558", description = "Regenerate uv.lock (gitignored)" }
t2_3 = { status = "completed", commit_sha = "20fa3558", description = "Delete requirements.txt" }
t2_4 = { status = "completed", commit_sha = "20fa3558", description = "Re-run audit + final.md report" }
t3_1 = { status = "completed", commit_sha = "a7ab994f", description = "Generate baseline file via --dump-baseline" }
t3_2 = { status = "completed", commit_sha = "a7ab994f", description = "Add --strict mode tests" }
t3_3 = { status = "completed", commit_sha = "a7ab994f", description = "Verify gate end-to-end (--strict exit 0)" }
t4_1 = { status = "completed", commit_sha = "TBD", description = "Add track entry to conductor/tracks.md" }
t4_2 = { status = "completed", commit_sha = "TBD", description = "Update state.toml to completed" }