#!/usr/bin/env python3 from __future__ import annotations import importlib.util import json import os from pathlib import Path import re import subprocess import sys import tempfile import unittest import yaml ROOT = Path(__file__).resolve().parents[2] SCRIPT = ROOT / "scripts" / "dependency_advisory_watch.py" WORKFLOW_PATH = ROOT / ".github" / "workflows" / "dependency-advisory-watch.yml" BUILD_AND_TEST = ROOT / ".github" / "workflows" / "build-and-test.yml" SPEC = importlib.util.spec_from_file_location("dependency_advisory_watch", SCRIPT) watch = importlib.util.module_from_spec(SPEC) assert SPEC.loader is not None SPEC.loader.exec_module(watch) FINDING = { "name": "dompurify", "severity": "moderate", "isDirect": True, "range": "<3.2.8", "via": [{"source": 1, "name": "dompurify", "title": "DOMPurify `bypass` | ", "url": "https://github.com/advisories/GHSA-p98j-92pf-mc4p", "severity": "moderate"}], "fixAvailable": {"name": "dompurify", "version": "3.2.8", "isSemVerMajor": False}, } TRANSITIVE = { "name": "brace-expansion", "severity": "low", "via": [{"url": "https://github.com/advisories/GHSA-q2hr-2g5m-vwhr", "title": "ReDoS"}, "minimatch"], "fixAvailable": True, } def audit_log(*findings: dict) -> str: rows = ["npm audit (all) attempt 1/3 (limit 60s, 240s of budget left)", "npm audit (all): vulnerabilities present (total=2)", "::error::npm audit (all) found vulnerabilities: total=2"] rows += ["audit finding " + json.dumps(f, sort_keys=True) for f in findings] return "\n".join(rows) + "\n" class SummaryTest(unittest.TestCase): def run_summary(self, branch: str, outcome: str, log: str) -> tuple[subprocess.CompletedProcess, str]: with tempfile.TemporaryDirectory() as directory: log_path = Path(directory) / "audit.log" summary = Path(directory) / "summary.md" log_path.write_text(log, encoding="utf-8") result = subprocess.run([sys.executable, str(SCRIPT), "summary", "--branch", branch, "--outcome", outcome, "--log", str(log_path), "--summary-file", str(summary)], capture_output=True, text=True, check=False) return result, summary.read_text(encoding="utf-8") if summary.exists() else "" def test_advisory_names_branch_advisories_and_fix(self) -> None: result, summary = self.run_summary("main", "failure", audit_log(FINDING, TRANSITIVE)) self.assertEqual(result.returncode, 0, result.stderr) self.assertIn("`main`", summary) self.assertIn("GHSA-p98j-92pf-mc4p", summary) self.assertIn("GHSA-q2hr-2g5m-vwhr", summary) self.assertIn("npm audit fix --package-lock-only", summary) self.assertIn("dependencySecurity.test.ts", summary) annotations = [line for line in result.stdout.splitlines() if line.startswith("::error")] self.assertEqual(len(annotations), 1) self.assertIn("main", annotations[0]) self.assertIn("GHSA-p98j-92pf-mc4p, GHSA-q2hr-2g5m-vwhr", annotations[0]) self.assertIn("lockfile advisory floor bump", annotations[0]) self.assertIn("qualify the resulting graph", summary) self.assertIn("land both on main", summary) self.assertIn("containing release", summary) self.assertIn("unchanged graphs only warn", summary) self.assertNotIn("block every pull request", summary) self.assertNotIn("blocks every PR and release", annotations[0]) def test_registry_text_is_escaped_and_kept_out_of_annotation(self) -> None: hostile = dict(FINDING, name="evil\n::warning::x", via=[{"title": "x |