diff --git a/scripts/actions/autofix/extract_python_vulns.py b/scripts/actions/autofix/extract_python_vulns.py index f26a099..a6de767 100644 --- a/scripts/actions/autofix/extract_python_vulns.py +++ b/scripts/actions/autofix/extract_python_vulns.py @@ -4,9 +4,63 @@ import sys from pathlib import Path +try: + import tomllib +except ImportError: + try: + import tomli as tomllib # type: ignore[no-redef] + except ImportError: + tomllib = None # type: ignore[assignment] + + +def extract_parents_from_lock(target_pkgs: list[str], lock_path: Path) -> list[str]: + """Inspects poetry.lock and returns direct parent packages declaring any target_pkgs.""" + if not lock_path.exists() or tomllib is None: + return [] + + targets = {p.strip().lower() for p in target_pkgs if p.strip()} + if not targets: + return [] + + try: + with open(lock_path, "rb") as f: + data = tomllib.load(f) + + parents: list[str] = [] + for pkg in data.get("package", []): + pkg_name = pkg.get("name", "") + if not pkg_name or pkg_name.lower() in targets: + continue + + deps = pkg.get("dependencies", {}) + dep_names = {k.lower() for k in deps.keys()} + if any(t in dep_names for t in targets): + parents.append(pkg_name) + + return list(dict.fromkeys(parents)) + except Exception: + return [] + def main() -> int: - json_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("pip-audit-findings.json") + args = sys.argv[1:] + + if "--parents-of" in args: + idx = args.index("--parents-of") + targets_raw = args[idx + 1 :] if idx + 1 < len(args) else [] + flat_targets: list[str] = [] + for item in targets_raw: + flat_targets.extend(item.replace(",", " ").split()) + + lock_path = Path("poetry.lock") + if not lock_path.exists() and Path("ml/poetry.lock").exists(): + lock_path = Path("ml/poetry.lock") + + parents = extract_parents_from_lock(flat_targets, lock_path) + print(" ".join(parents)) + return 0 + + json_path = Path(args[0]) if args else Path("pip-audit-findings.json") if not json_path.exists(): print("") return 0 diff --git a/scripts/actions/autofix/remediate_python.sh b/scripts/actions/autofix/remediate_python.sh index a9162fd..c37d02e 100644 --- a/scripts/actions/autofix/remediate_python.sh +++ b/scripts/actions/autofix/remediate_python.sh @@ -21,9 +21,25 @@ poetry run pip-audit --format json --skip-editable > pip-audit-findings.json 2>/ VULN_PKGS=$(python3 ../scripts/actions/autofix/extract_python_vulns.py pip-audit-findings.json) if [ -n "$VULN_PKGS" ]; then - echo "Dynamically detected vulnerable Python packages: $VULN_PKGS" + echo "Tier 1: Dynamically detected vulnerable Python packages: $VULN_PKGS" # shellcheck disable=SC2086 poetry update $VULN_PKGS + + # Re-audit to verify if any packages remain unpatched due to transitive parent constraints + echo "Checking if any vulnerabilities persist after Tier 1 update..." + poetry run pip-audit --format json --skip-editable > pip-audit-findings.json 2>/dev/null || true + REMAINING_VULNS=$(python3 ../scripts/actions/autofix/extract_python_vulns.py pip-audit-findings.json) + + if [ -n "$REMAINING_VULNS" ]; then + echo "Vulnerabilities still present after leaf update: $REMAINING_VULNS" + # shellcheck disable=SC2086 + PARENT_PKGS=$(python3 ../scripts/actions/autofix/extract_python_vulns.py --parents-of $REMAINING_VULNS) + if [ -n "$PARENT_PKGS" ]; then + echo "Tier 2: Expanding update to include constraining parent packages: $PARENT_PKGS" + # shellcheck disable=SC2086 + poetry update $REMAINING_VULNS $PARENT_PKGS + fi + fi else echo "No additional vulnerable Python packages detected by dynamic extractor." fi