Fix audit tool bootstrap and add per-run preflight
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.
This commit is contained in:
@@ -187,6 +187,51 @@ def test_cli_writes_per_agent_slices(tmp_path):
|
||||
assert sliced["trivy_findings"] == full["trivy_findings"]
|
||||
|
||||
|
||||
def test_cli_records_missing_scanners_with_install_command(tmp_path):
|
||||
repo = _stage_fixture(tmp_path / "repo")
|
||||
out_dir = tmp_path / "out"
|
||||
mod = _load_cli()
|
||||
ran: list[str] = []
|
||||
|
||||
def _fake(cmd, **kw):
|
||||
ran.append(cmd[0])
|
||||
return _fake_subprocess(cmd, **kw)
|
||||
|
||||
with patch("subprocess.run", side_effect=_fake), \
|
||||
patch("shutil.which", side_effect=lambda t: None if t in ("trivy", "tflint") else f"/usr/bin/{t}"):
|
||||
rc = mod.main([
|
||||
"--repo", str(repo), "--base", "main", "--head", "HEAD",
|
||||
"--output-dir", str(out_dir), "--mode", "local",
|
||||
])
|
||||
assert rc == 0
|
||||
manifest = json.loads((out_dir / "manifest.json").read_text())
|
||||
assert manifest["tools_unavailable"] == {
|
||||
"trivy": "go install github.com/aquasecurity/trivy/cmd/trivy@latest",
|
||||
"tflint": "go install github.com/terraform-linters/tflint@latest",
|
||||
}
|
||||
assert "trivy" not in ran
|
||||
assert manifest["trivy_findings"] == []
|
||||
|
||||
|
||||
|
||||
def test_cli_stops_with_install_command_when_plan_tool_missing(tmp_path):
|
||||
repo = _stage_fixture(tmp_path / "repo")
|
||||
out_dir = tmp_path / "out"
|
||||
mod = _load_cli()
|
||||
with patch("subprocess.run", side_effect=_fake_subprocess), \
|
||||
patch("shutil.which", side_effect=lambda t: None if t == "tofu" else f"/usr/bin/{t}"):
|
||||
rc = mod.main([
|
||||
"--repo", str(repo), "--base", "main", "--head", "HEAD",
|
||||
"--output-dir", str(out_dir), "--mode", "local",
|
||||
])
|
||||
assert rc == 1
|
||||
manifest = json.loads((out_dir / "manifest.json").read_text())
|
||||
install = "go install github.com/opentofu/opentofu/cmd/tofu@latest"
|
||||
assert manifest["tools_unavailable"] == {"tofu": install}
|
||||
assert manifest["errors"] == [
|
||||
f"tofu is not installed, so the changed units cannot be planned. Install it with `{install}` and re-run."
|
||||
]
|
||||
|
||||
def test_cli_preserves_terragrunt_only_change_context(tmp_path):
|
||||
repo = _stage_fixture(tmp_path / "repo")
|
||||
terragrunt_dir = repo / "live" / "prod" / "app"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, UTC
|
||||
|
||||
from scripts.controls_schema import Control, ControlsFile
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_controls_file_serializes_with_resource_type_index():
|
||||
)
|
||||
cf = ControlsFile(
|
||||
source="fsbp",
|
||||
fetched_at=datetime(2026, 5, 12, 9, 0, 0, tzinfo=timezone.utc),
|
||||
fetched_at=datetime(2026, 5, 12, 9, 0, 0, tzinfo=UTC),
|
||||
controls=[a, b],
|
||||
)
|
||||
payload = json.loads(cf.to_json())
|
||||
@@ -50,7 +50,7 @@ def test_control_appears_under_every_resource_type():
|
||||
)
|
||||
cf = ControlsFile(
|
||||
source="fsbp",
|
||||
fetched_at=datetime(2026, 5, 12, tzinfo=timezone.utc),
|
||||
fetched_at=datetime(2026, 5, 12, tzinfo=UTC),
|
||||
controls=[c],
|
||||
)
|
||||
payload = json.loads(cf.to_json())
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Tests for scripts/install-tools.sh --check-only, run against a copy with stubbed binaries."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT = Path(__file__).resolve().parent.parent / "scripts" / "install-tools.sh"
|
||||
|
||||
_NATIVE_TOOLS = ("trivy", "tflint", "tofu", "terragrunt", "gh")
|
||||
|
||||
|
||||
def _skill_copy(tmp_path: Path) -> Path:
|
||||
skill = tmp_path / "skill"
|
||||
(skill / "scripts").mkdir(parents=True)
|
||||
shutil.copy(_SCRIPT, skill / "scripts" / "install-tools.sh")
|
||||
return skill / "scripts" / "install-tools.sh"
|
||||
|
||||
|
||||
def _stub(bin_dir: Path, name: str, body: str = "") -> None:
|
||||
bin_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = bin_dir / name
|
||||
path.write_text("#!/usr/bin/env bash\n" + body)
|
||||
path.chmod(0o755)
|
||||
|
||||
|
||||
def _check_only(script: Path, bin_dir: Path) -> subprocess.CompletedProcess:
|
||||
# Only the commands the script itself needs, so real trivy/tofu on this host can't leak in.
|
||||
core = bin_dir.parent / "core"
|
||||
core.mkdir(exist_ok=True)
|
||||
for tool in ("bash", "dirname"):
|
||||
(core / tool).symlink_to(shutil.which(tool))
|
||||
bin_dir.mkdir(exist_ok=True)
|
||||
return subprocess.run(
|
||||
["bash", str(script), "--check-only"],
|
||||
capture_output=True, text=True, timeout=60, check=False,
|
||||
env={**os.environ, "PATH": f"{bin_dir}:{core}"},
|
||||
)
|
||||
|
||||
|
||||
def test_check_only_on_empty_venv_fails_and_names_missing(tmp_path: Path) -> None:
|
||||
script = _skill_copy(tmp_path)
|
||||
r = _check_only(script, tmp_path / "bin")
|
||||
assert r.returncode != 0
|
||||
for name in ("python-hcl2", "trivy", "tflint", "tofu", "terragrunt"):
|
||||
assert name in r.stdout, r.stdout
|
||||
assert not (script.parent.parent / ".venv").exists(), "--check-only must not install anything"
|
||||
|
||||
|
||||
def test_check_only_passes_when_everything_present(tmp_path: Path) -> None:
|
||||
script = _skill_copy(tmp_path)
|
||||
_stub(script.parent.parent / ".venv" / "bin", "python")
|
||||
bin_dir = tmp_path / "bin"
|
||||
for tool in _NATIVE_TOOLS:
|
||||
_stub(bin_dir, tool)
|
||||
r = _check_only(script, bin_dir)
|
||||
assert r.returncode == 0, r.stdout + r.stderr
|
||||
@@ -3,6 +3,9 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SPEC = importlib.util.spec_from_file_location(
|
||||
@@ -91,3 +94,17 @@ def test_malformed_findings_file_counts_zero(tmp_path: Path) -> None:
|
||||
])
|
||||
assert rc == 0
|
||||
assert _read_log(log)[0]["finding_count"] == 0
|
||||
|
||||
|
||||
def test_runs_as_a_script_from_another_cwd(tmp_path: Path) -> None:
|
||||
log = tmp_path / "runs.jsonl"
|
||||
env = {k: v for k, v in os.environ.items() if k != "PYTHONPATH"}
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(_SPEC.origin),
|
||||
"--output-dir", str(tmp_path), "--run-id", "x", "--repo", "/r",
|
||||
"--mode", "local", "--log-path", str(log), "--usage-json", "-"],
|
||||
input=json.dumps({"x-reviewer": {"model": "sonnet"}}),
|
||||
capture_output=True, text=True, cwd=tmp_path, env=env, check=False,
|
||||
)
|
||||
assert r.returncode == 0, r.stderr
|
||||
assert _read_log(log)[0]["agent"] == "x-reviewer"
|
||||
|
||||
Reference in New Issue
Block a user