audit-code install-tools.sh:
- Buffer the opengrep release JSON before grep -m1; curl died with (23)
under pipefail when grep quit early.
- Use ${m}: in the PowerShell block; $m: parsed as a scope-qualified var.
- On Arch, skip paru/yay when pacman -Q shows every package installed,
since --needed still invokes sudo.
- Add --check-only (fast, installs nothing, non-zero naming missing tools)
and --user-only (no system package managers, no sudo).
log-run.py (both skills): put the skill dir on sys.path so running it as
a script from any cwd no longer raises ModuleNotFoundError.
audit-terraform: move deps from requirements.txt into pyproject
dependency groups and add scripts/install-tools.sh (uv sync --group tools,
then check trivy, tflint, tofu, terragrunt, gh).
Both SKILL.md files gain a 0.5 Preflight step and call scripts through
uv run --project ${SKILL_DIR}. tools_unavailable is now a map of tool to
exact install command; audit-terraform skips trivy when absent and stops
with an install hint instead of crashing when tofu/terragrunt is missing.
197 lines
7.3 KiB
Python
197 lines
7.3 KiB
Python
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
_SKILL_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
def _load_cli():
|
|
spec = importlib.util.spec_from_file_location(
|
|
"collect_findings", _SKILL_ROOT / "scripts" / "collect-findings.py"
|
|
)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def _fake_subprocess(cmd, **kwargs):
|
|
if cmd[:2] == ["git", "-C"]:
|
|
sub = cmd[2:]
|
|
else:
|
|
sub = cmd
|
|
if "symbolic-ref" in sub:
|
|
return MagicMock(returncode=0, stdout="refs/remotes/origin/main\n", stderr="")
|
|
if "fetch" in sub:
|
|
return MagicMock(returncode=0, stdout="", stderr="")
|
|
if "rev-parse" in sub and "--verify" in sub:
|
|
return MagicMock(returncode=0, stdout="abc\n", stderr="")
|
|
if cmd[:1] == ["git"] and "diff" in cmd:
|
|
diff = (
|
|
"diff --git a/src/api.py b/src/api.py\n"
|
|
"index 1..2 100644\n"
|
|
"--- a/src/api.py\n"
|
|
"+++ b/src/api.py\n"
|
|
"@@ -12 +12,1 @@\n"
|
|
"+x = parse(user_input)\n"
|
|
)
|
|
return MagicMock(returncode=0, stdout=diff, stderr="")
|
|
if Path(cmd[0]).name == "bandit":
|
|
out = json.dumps({"results": [{
|
|
"filename": "src/api.py", "line_number": 12, "line_range": [12, 12],
|
|
"issue_severity": "HIGH", "issue_text": "Use of dynamic parsing", "test_id": "B307",
|
|
}]})
|
|
return MagicMock(returncode=0, stdout=out, stderr="")
|
|
return MagicMock(returncode=0, stdout="", stderr="")
|
|
|
|
|
|
def test_cli_aborts_when_no_supported_files(tmp_path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
out_dir = tmp_path / "out"
|
|
mod = _load_cli()
|
|
def _fake(cmd, **kw):
|
|
if cmd[:1] == ["git"] and "diff" in cmd:
|
|
return MagicMock(returncode=0,
|
|
stdout="diff --git a/README.md b/README.md\nindex 1..2 100644\n--- a/README.md\n+++ b/README.md\n@@ -1 +1,1 @@\n+x\n",
|
|
stderr="")
|
|
return _fake_subprocess(cmd, **kw)
|
|
with patch("subprocess.run", side_effect=_fake):
|
|
with patch("shutil.which", return_value=None):
|
|
rc = mod.main([
|
|
"--repo", str(repo),
|
|
"--head", "HEAD",
|
|
"--output-dir", str(out_dir),
|
|
"--mode", "local",
|
|
])
|
|
assert rc == 1
|
|
manifest = json.loads((out_dir / "manifest.json").read_text())
|
|
assert any("no supported source files" in e.lower() for e in manifest["errors"])
|
|
|
|
|
|
def test_cli_happy_path_python_only(tmp_path):
|
|
from scripts import runner
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / "src").mkdir()
|
|
(repo / "src" / "api.py").write_text("x = 1\n")
|
|
out_dir = tmp_path / "out"
|
|
mod = _load_cli()
|
|
empty_venv = tmp_path / "empty-venv"
|
|
empty_venv.mkdir()
|
|
def _which(binary):
|
|
return "/usr/bin/bandit" if binary == "bandit" else None
|
|
with patch.object(runner, "_SKILL_VENV_BIN", empty_venv):
|
|
with patch("subprocess.run", side_effect=_fake_subprocess):
|
|
with patch("shutil.which", side_effect=_which):
|
|
rc = mod.main([
|
|
"--repo", str(repo),
|
|
"--head", "HEAD",
|
|
"--output-dir", str(out_dir),
|
|
"--mode", "local",
|
|
])
|
|
assert rc == 0
|
|
manifest = json.loads((out_dir / "manifest.json").read_text())
|
|
assert manifest["mode"] == "local"
|
|
assert manifest["language_breakdown"]["python"] == 1
|
|
rule_ids = {f["rule_id"] for f in manifest["findings"]}
|
|
assert "B307" in rule_ids
|
|
for agent in ("security-triage", "type-safety", "dependency", "consistency", "secrets"):
|
|
assert (out_dir / f"manifest-{agent}.json").exists()
|
|
|
|
|
|
def test_cli_records_tool_unavailable(tmp_path):
|
|
from scripts import runner
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / "src").mkdir()
|
|
(repo / "src" / "api.py").write_text("x = 1\n")
|
|
out_dir = tmp_path / "out"
|
|
mod = _load_cli()
|
|
empty_venv = tmp_path / "empty-venv"
|
|
empty_venv.mkdir()
|
|
with patch.object(runner, "_SKILL_VENV_BIN", empty_venv):
|
|
with patch("subprocess.run", side_effect=_fake_subprocess):
|
|
with patch("shutil.which", return_value=None):
|
|
rc = mod.main([
|
|
"--repo", str(repo),
|
|
"--head", "HEAD",
|
|
"--output-dir", str(out_dir),
|
|
"--mode", "local",
|
|
])
|
|
assert rc == 0
|
|
manifest = json.loads((out_dir / "manifest.json").read_text())
|
|
assert "bandit" in manifest["tools_unavailable"]
|
|
assert manifest["tools_unavailable"]["bandit"].endswith("scripts/install-tools.sh --user-only")
|
|
|
|
|
|
def test_cli_alerts_on_unsupported_languages(tmp_path):
|
|
"""Mixed PR: Python supported, Go file alerts — but review still proceeds."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / "src").mkdir()
|
|
(repo / "src" / "api.py").write_text("x = 1\n")
|
|
out_dir = tmp_path / "out"
|
|
mod = _load_cli()
|
|
|
|
def _fake(cmd, **kw):
|
|
if cmd[:1] == ["git"] and "diff" in cmd:
|
|
diff = (
|
|
"diff --git a/src/api.py b/src/api.py\n"
|
|
"index 1..2 100644\n--- a/src/api.py\n+++ b/src/api.py\n"
|
|
"@@ -1 +1,1 @@\n+x = 1\n"
|
|
"diff --git a/cmd/server.go b/cmd/server.go\n"
|
|
"index 3..4 100644\n--- a/cmd/server.go\n+++ b/cmd/server.go\n"
|
|
"@@ -1 +1,1 @@\n+package main\n"
|
|
)
|
|
return MagicMock(returncode=0, stdout=diff, stderr="")
|
|
return _fake_subprocess(cmd, **kw)
|
|
|
|
with patch("subprocess.run", side_effect=_fake):
|
|
with patch("shutil.which", return_value=None):
|
|
rc = mod.main([
|
|
"--repo", str(repo),
|
|
"--head", "HEAD",
|
|
"--output-dir", str(out_dir),
|
|
"--mode", "local",
|
|
])
|
|
|
|
assert rc == 0, "review should continue when at least one supported file present"
|
|
manifest = json.loads((out_dir / "manifest.json").read_text())
|
|
assert any("Go" in e and "server.go" in e for e in manifest["errors"]), manifest["errors"]
|
|
assert manifest["language_breakdown"]["python"] == 1
|
|
|
|
|
|
def test_cli_alerts_on_unsupported_only_and_aborts(tmp_path):
|
|
"""Diff contains ONLY unsupported languages: alert + abort."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
out_dir = tmp_path / "out"
|
|
mod = _load_cli()
|
|
|
|
def _fake(cmd, **kw):
|
|
if cmd[:1] == ["git"] and "diff" in cmd:
|
|
diff = (
|
|
"diff --git a/cmd/server.go b/cmd/server.go\n"
|
|
"index 3..4 100644\n--- a/cmd/server.go\n+++ b/cmd/server.go\n"
|
|
"@@ -1 +1,1 @@\n+package main\n"
|
|
)
|
|
return MagicMock(returncode=0, stdout=diff, stderr="")
|
|
return _fake_subprocess(cmd, **kw)
|
|
|
|
with patch("subprocess.run", side_effect=_fake):
|
|
with patch("shutil.which", return_value=None):
|
|
rc = mod.main([
|
|
"--repo", str(repo),
|
|
"--head", "HEAD",
|
|
"--output-dir", str(out_dir),
|
|
"--mode", "local",
|
|
])
|
|
|
|
assert rc == 1
|
|
manifest = json.loads((out_dir / "manifest.json").read_text())
|
|
errors_blob = " | ".join(manifest["errors"])
|
|
assert "Go" in errors_blob, errors_blob
|
|
assert "no supported source files" in errors_blob.lower()
|