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:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import concurrent.futures as cf
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
@@ -32,6 +33,13 @@ from scripts.source_lookup import find_block
|
||||
|
||||
_PLAN_CONCURRENCY = 8
|
||||
|
||||
_INSTALL_COMMANDS = {
|
||||
"trivy": "go install github.com/aquasecurity/trivy/cmd/trivy@latest",
|
||||
"tflint": "go install github.com/terraform-linters/tflint@latest",
|
||||
"tofu": "go install github.com/opentofu/opentofu/cmd/tofu@latest",
|
||||
"terragrunt": "go install github.com/gruntwork-io/terragrunt@latest",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_default_branch(repo: Path) -> str:
|
||||
try:
|
||||
@@ -197,6 +205,28 @@ def main(argv: list[str] | None = None) -> int:
|
||||
for orphan in orphan_modules:
|
||||
errors.append(f"module has no callsites; diff-only review: {orphan}")
|
||||
|
||||
plan_tools = sorted({detect_tool(repo / pd) for pd in plan_units_map})
|
||||
tools_unavailable = {
|
||||
t: _INSTALL_COMMANDS[t] for t in ("trivy", "tflint", *plan_tools)
|
||||
if shutil.which(t) is None
|
||||
}
|
||||
missing_plan_tools = [t for t in plan_tools if t in tools_unavailable]
|
||||
if missing_plan_tools:
|
||||
manifest = Manifest(
|
||||
base_ref=base, head_ref=args.head, mode=args.mode,
|
||||
default_branch=default_branch,
|
||||
changed_source_dirs=sorted(dirs), plan_units=[], catalog=[],
|
||||
trivy_findings=[], module_graph=module_graph,
|
||||
errors=[
|
||||
f"{t} is not installed, so the changed units cannot be planned. "
|
||||
f"Install it with `{tools_unavailable[t]}` and re-run."
|
||||
for t in missing_plan_tools
|
||||
],
|
||||
tools_unavailable=tools_unavailable,
|
||||
)
|
||||
(out_dir / "manifest.json").write_text(manifest.to_json())
|
||||
return 1
|
||||
|
||||
plan_hits: dict[str, list[PlanHit]] = {}
|
||||
plan_unit_records: list[PlanUnit] = []
|
||||
|
||||
@@ -250,7 +280,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
f"terragrunt change has no planned unit context: {terragrunt_file}"
|
||||
)
|
||||
|
||||
trivy_payload, trivy_findings, trivy_error = _run_trivy_config(repo, files)
|
||||
trivy_payload, trivy_findings, trivy_error = (
|
||||
({}, [], None) if "trivy" in tools_unavailable else _run_trivy_config(repo, files)
|
||||
)
|
||||
(out_dir / "trivy-findings.json").write_text(json.dumps(trivy_payload, indent=2))
|
||||
if trivy_error:
|
||||
errors.append(trivy_error)
|
||||
@@ -293,6 +325,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
tflint_findings=tflint_findings,
|
||||
module_graph=module_graph,
|
||||
errors=errors,
|
||||
tools_unavailable=tools_unavailable,
|
||||
)
|
||||
(out_dir / "manifest.json").write_text(manifest.to_json())
|
||||
manifest_dict = manifest.to_dict()
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
say() { printf '\n\033[1m▶ %s\033[0m\n' "$*"; }
|
||||
warn() { printf '\033[33m! %s\033[0m\n' "$*"; }
|
||||
|
||||
|
||||
install_python_tools() {
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
warn "uv not installed. Install: https://docs.astral.sh/uv/getting-started/installation/"
|
||||
exit 1
|
||||
fi
|
||||
say "Installing Python dependencies into $SKILL_DIR/.venv/ via uv"
|
||||
uv sync --group tools
|
||||
}
|
||||
|
||||
# Each plugin version gets its own venv, so the check targets this skill's .venv, not whatever python is on PATH.
|
||||
python_deps_present() {
|
||||
[[ -x "$SKILL_DIR/.venv/bin/python" ]] &&
|
||||
"$SKILL_DIR/.venv/bin/python" -c 'import hcl2, bs4, requests' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
verify_tools() {
|
||||
say "Verifying tool availability"
|
||||
local tool missing=()
|
||||
if python_deps_present; then
|
||||
printf ' %-15s %s\n' "python deps" "(.venv)"
|
||||
else
|
||||
missing+=("python-hcl2/beautifulsoup4/requests (.venv)")
|
||||
fi
|
||||
# tofu only: the plan runner has no terraform fallback.
|
||||
for tool in trivy tflint tofu terragrunt gh; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
printf ' %-15s %s\n' "$tool" "$(command -v "$tool")"
|
||||
else
|
||||
missing+=("$tool")
|
||||
fi
|
||||
done
|
||||
[[ ${#missing[@]} -eq 0 ]] && return
|
||||
printf ' %-15s \033[31mmissing\033[0m\n' "${missing[@]}"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
case "${1:-}" in
|
||||
"") ;;
|
||||
--check-only) verify_tools; return ;;
|
||||
*) echo "usage: install-tools.sh [--check-only]" >&2; return 2 ;;
|
||||
esac
|
||||
|
||||
cd "$SKILL_DIR"
|
||||
install_python_tools
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Native tools are not installed here. Missing ones install user-scoped with:
|
||||
trivy go install github.com/aquasecurity/trivy/cmd/trivy@latest
|
||||
tflint go install github.com/terraform-linters/tflint@latest
|
||||
tofu go install github.com/opentofu/opentofu/cmd/tofu@latest
|
||||
terragrunt go install github.com/gruntwork-io/terragrunt@latest
|
||||
gh go install github.com/cli/cli/v2/cmd/gh@latest
|
||||
|
||||
Run /audit-terraform to use the skill.
|
||||
EOF
|
||||
verify_tools
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
@@ -25,6 +25,8 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from scripts.telemetry import append_subagent_run
|
||||
|
||||
_DEFAULT_LOG = Path.home() / ".claude/cache/audit-terraform/runs.jsonl"
|
||||
|
||||
@@ -162,6 +162,7 @@ class Manifest:
|
||||
module_graph: dict[str, ModuleGraphEntry]
|
||||
errors: list[str]
|
||||
tflint_findings: list[TflintFinding] = field(default_factory=list)
|
||||
tools_unavailable: dict[str, str] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -176,6 +177,7 @@ class Manifest:
|
||||
"tflint_findings": [f.to_dict() for f in self.tflint_findings],
|
||||
"module_graph": {k: v.to_dict() for k, v in self.module_graph.items()},
|
||||
"errors": list(self.errors),
|
||||
"tools_unavailable": dict(self.tools_unavailable),
|
||||
}
|
||||
|
||||
def to_json(self, indent: int = 2) -> str:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, UTC
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
@@ -29,7 +29,7 @@ def _fetch(url: str) -> str:
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
def _build_fsbp() -> ControlsFile:
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timezone, UTC
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def _append(log_path: Path, record: dict) -> None:
|
||||
|
||||
Reference in New Issue
Block a user