"""GuardDog CLI integration via subprocess.""" import json import shutil import subprocess from guarddog_nexus.config import config from guarddog_nexus.logging_setup import log GUARDDOG_BIN = shutil.which("guarddog") or "guarddog" def scan_package(filepath: str, ecosystem: str = "pypi") -> dict: """Run guarddog scan on a downloaded package file. Returns parsed JSON output.""" cmd = [ GUARDDOG_BIN, ecosystem, "scan", filepath, "--output-format", "json", ] log.info("Running: %s", " ".join(cmd)) try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=config.scan_timeout_seconds, ) except subprocess.TimeoutExpired: log.error("GuardDog scan timed out for %s", filepath) return {"issues": [], "errors": ["timeout"]} except FileNotFoundError: log.error("GuardDog binary not found at %s", GUARDDOG_BIN) return {"issues": [], "errors": ["guarddog_not_found"]} if result.returncode not in (0, 1): log.error("GuardDog exited %d: %s", result.returncode, result.stderr) return {"issues": [], "errors": [result.stderr.strip()]} try: data = json.loads(result.stdout) except json.JSONDecodeError: log.error("GuardDog returned invalid JSON for %s", filepath) return {"issues": [], "errors": ["json_parse_error"]} return _normalize_output(data) def _normalize_output(data: dict) -> dict: """Normalize guarddog JSON output across versions into a consistent format. GuardDog JSON format (varies by version): { "results": [{"rule": "...", "severity": "...", "message": "...", "location": "..."}], "errors": [...] } Or simpler: {"issues": [...], "errors": [...]} """ findings = [] for entry in data.get("results", data.get("issues", [])): if isinstance(entry, dict): findings.append({ "rule": entry.get("rule", entry.get("id", "unknown")), "severity": entry.get("severity", "WARNING"), "message": entry.get("message", entry.get("description", "")), "location": entry.get("location", entry.get("path", "")), }) return { "findings": findings, "errors": data.get("errors", []), }