feat: guarddog-nexus — webhook-based PyPI scanner with web UI
This commit is contained in:
74
guarddog_nexus/scanner.py
Normal file
74
guarddog_nexus/scanner.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""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", []),
|
||||
}
|
||||
Reference in New Issue
Block a user