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:
@@ -54,6 +54,23 @@ from scripts.runner import run_tool, tool_available
|
||||
from scripts.slicing import slice_for_agent
|
||||
|
||||
|
||||
# Anything not listed here is installed by the skill's own bootstrap script.
|
||||
_INSTALL_TOOLS = f"bash {_HERE / 'install-tools.sh'} --user-only"
|
||||
_INSTALL_COMMANDS = {
|
||||
"eslint": "npm i -D eslint eslint-plugin-security",
|
||||
"tsc": "npm i -D typescript",
|
||||
"knip": "npm i -D knip",
|
||||
"jscpd": "npm i -D jscpd",
|
||||
"dotnet": "curl -fsSL https://dot.net/v1/dotnet-install.sh | bash",
|
||||
"selene": "cargo install selene",
|
||||
"luac": "install lua (ships luac) with your OS package manager",
|
||||
"actionlint": "go install github.com/rhysd/actionlint/cmd/actionlint@latest",
|
||||
"zizmor": "uv tool install zizmor",
|
||||
"gitleaks": "go install github.com/zricethezav/gitleaks/v8@latest",
|
||||
"osv-scanner": "go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest",
|
||||
}
|
||||
|
||||
|
||||
def _git_show(repo: str, ref: str, path: str) -> str:
|
||||
"""Return file content at `ref`, or empty string if not present (e.g. new file)."""
|
||||
r = subprocess.run(
|
||||
@@ -350,7 +367,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
default_branch=default_branch,
|
||||
language_breakdown=LanguageBreakdown(),
|
||||
changed_files=[], findings=[],
|
||||
package_diffs={}, tool_stats={}, tools_unavailable=[],
|
||||
package_diffs={}, tool_stats={}, tools_unavailable={},
|
||||
errors=[str(e)],
|
||||
)
|
||||
(out_dir / "manifest.json").write_text(manifest.to_json())
|
||||
@@ -396,7 +413,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
mode=args.mode, base_ref=base, head_ref=args.head,
|
||||
default_branch=default_branch, language_breakdown=breakdown,
|
||||
changed_files=[], findings=[],
|
||||
package_diffs={}, tool_stats={}, tools_unavailable=[],
|
||||
package_diffs={}, tool_stats={}, tools_unavailable={},
|
||||
errors=errors,
|
||||
)
|
||||
(out_dir / "manifest.json").write_text(manifest.to_json())
|
||||
@@ -433,7 +450,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
if stat.ran:
|
||||
stat.post_filter = sum(1 for f in filtered if f.tool == tool_name)
|
||||
|
||||
tools_unavailable = [name for name, s in tool_stats.items() if not s.ran]
|
||||
tools_unavailable = {
|
||||
name: _INSTALL_COMMANDS.get(name, _INSTALL_TOOLS)
|
||||
for name, s in tool_stats.items() if not s.ran
|
||||
}
|
||||
|
||||
package_diffs = _build_package_diffs(repo, base, dep_manifest_paths)
|
||||
|
||||
|
||||
@@ -3,24 +3,25 @@
|
||||
set -euo pipefail
|
||||
|
||||
SKILL_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$SKILL_DIR"
|
||||
|
||||
say() { printf '\n\033[1m▶ %s\033[0m\n' "$*"; }
|
||||
warn() { printf '\033[33m! %s\033[0m\n' "$*"; }
|
||||
|
||||
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
warn "uv not installed. Install: https://docs.astral.sh/uv/getting-started/installation/"
|
||||
warn "Falling back to plain pip — tools will install into the active environment."
|
||||
if ! command -v pip >/dev/null 2>&1; then
|
||||
echo "Neither uv nor pip available. Aborting Python-tools install."
|
||||
exit 1
|
||||
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/"
|
||||
warn "Falling back to plain pip — tools will install into the active environment."
|
||||
if ! command -v pip >/dev/null 2>&1; then
|
||||
echo "Neither uv nor pip available. Aborting Python-tools install."
|
||||
exit 1
|
||||
fi
|
||||
pip install bandit ruff mypy pip-audit
|
||||
else
|
||||
say "Installing Python tools into $SKILL_DIR/.venv/ via uv"
|
||||
uv sync --group tools
|
||||
fi
|
||||
pip install bandit ruff mypy pip-audit
|
||||
else
|
||||
say "Installing Python tools into $SKILL_DIR/.venv/ via uv"
|
||||
uv sync --group tools
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
install_opengrep() {
|
||||
@@ -42,8 +43,10 @@ install_opengrep() {
|
||||
return
|
||||
;;
|
||||
esac
|
||||
local tag url
|
||||
tag="$(curl -fsSL https://api.github.com/repos/opengrep/opengrep/releases/latest | grep -m1 '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
local release tag url
|
||||
# Buffer the response: piping curl into an early-exiting `grep -m1` makes curl die with (23), which pipefail turns fatal.
|
||||
release="$(curl -fsSL https://api.github.com/repos/opengrep/opengrep/releases/latest)"
|
||||
tag="$(grep -m1 '"tag_name"' <<<"$release" | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
if [[ -z "$tag" ]]; then
|
||||
warn "opengrep: could not resolve latest release tag, install manually"
|
||||
return
|
||||
@@ -54,8 +57,6 @@ install_opengrep() {
|
||||
chmod +x "$SKILL_DIR/.venv/bin/opengrep"
|
||||
}
|
||||
|
||||
install_opengrep
|
||||
|
||||
|
||||
install_powershell_modules() {
|
||||
if ! command -v pwsh >/dev/null 2>&1; then
|
||||
@@ -67,16 +68,14 @@ install_powershell_modules() {
|
||||
pwsh -NoProfile -NonInteractive -Command '
|
||||
foreach ($m in "PSScriptAnalyzer", "InjectionHunter") {
|
||||
if (Get-Module -ListAvailable -Name $m) {
|
||||
Write-Host " $m: already installed"
|
||||
Write-Host " ${m}: already installed"
|
||||
} else {
|
||||
Install-Module -Name $m -Scope CurrentUser -Force -AcceptLicense -Repository PSGallery
|
||||
Write-Host " $m: installed"
|
||||
Write-Host " ${m}: installed"
|
||||
}
|
||||
}'
|
||||
}
|
||||
|
||||
install_powershell_modules
|
||||
|
||||
|
||||
install_native_brew() {
|
||||
say "Installing native tools via Homebrew"
|
||||
@@ -113,7 +112,15 @@ install_native_arch() {
|
||||
fi
|
||||
say "Installing native tools via $helper"
|
||||
|
||||
local pkgs=(gitleaks github-cli osv-scanner)
|
||||
# --needed still invokes sudo, so skip the helper entirely when nothing is missing.
|
||||
local pkgs=() pkg
|
||||
for pkg in gitleaks github-cli osv-scanner; do
|
||||
pacman -Q "$pkg" >/dev/null 2>&1 || pkgs+=("$pkg")
|
||||
done
|
||||
if [[ ${#pkgs[@]} -eq 0 ]]; then
|
||||
echo " gitleaks, github-cli, osv-scanner: already installed"
|
||||
return
|
||||
fi
|
||||
if [[ "$helper" == "pacman" ]]; then
|
||||
sudo pacman -S --needed --noconfirm gitleaks github-cli || true
|
||||
if ! command -v osv-scanner >/dev/null 2>&1; then
|
||||
@@ -125,29 +132,66 @@ install_native_arch() {
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
install_native_brew
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
install_native_arch
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
install_native_apt
|
||||
else
|
||||
warn "No supported native package manager found (brew/pacman/apt). Install gitleaks, osv-scanner, gh manually."
|
||||
fi
|
||||
|
||||
|
||||
say "Verifying tool availability"
|
||||
for tool in bandit ruff mypy pip-audit opengrep vulture radon interrogate lizard gitleaks osv-scanner gh pwsh; do
|
||||
if [[ -x "$SKILL_DIR/.venv/bin/$tool" ]]; then
|
||||
printf ' %-15s %s\n' "$tool" "(.venv/bin)"
|
||||
elif command -v "$tool" >/dev/null 2>&1; then
|
||||
printf ' %-15s %s\n' "$tool" "$(command -v "$tool")"
|
||||
install_native() {
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
install_native_brew
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
install_native_arch
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
install_native_apt
|
||||
else
|
||||
printf ' %-15s \033[31mmissing\033[0m\n' "$tool"
|
||||
warn "No supported native package manager found (brew/pacman/apt). Install gitleaks, osv-scanner, gh manually."
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cat <<EOF
|
||||
# Checked in .venv/bin only: each plugin version gets its own venv, and a copy on PATH says nothing about this one.
|
||||
VENV_TOOLS=(bandit ruff mypy pip-audit vulture radon interrogate lizard opengrep)
|
||||
NATIVE_TOOLS=(gitleaks osv-scanner gh)
|
||||
|
||||
verify_tools() {
|
||||
say "Verifying tool availability"
|
||||
local tool missing=()
|
||||
for tool in "${VENV_TOOLS[@]}"; do
|
||||
if [[ -x "$SKILL_DIR/.venv/bin/$tool" ]]; then
|
||||
printf ' %-15s %s\n' "$tool" "(.venv/bin)"
|
||||
else
|
||||
missing+=("$tool")
|
||||
fi
|
||||
done
|
||||
for tool in "${NATIVE_TOOLS[@]}" pwsh; do
|
||||
if command -v "$tool" >/dev/null 2>&1; then
|
||||
printf ' %-15s %s\n' "$tool" "$(command -v "$tool")"
|
||||
elif [[ "$tool" == pwsh ]]; then
|
||||
printf ' %-15s %s\n' "$tool" "missing (optional: PowerShell review only)"
|
||||
else
|
||||
missing+=("$tool")
|
||||
fi
|
||||
done
|
||||
[[ ${#missing[@]} -eq 0 ]] && return
|
||||
printf ' %-15s \033[31mmissing\033[0m\n' "${missing[@]}"
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
local user_only=0
|
||||
case "${1:-}" in
|
||||
"") ;;
|
||||
--check-only) verify_tools; return ;;
|
||||
--user-only) user_only=1 ;;
|
||||
*) echo "usage: install-tools.sh [--check-only | --user-only]" >&2; return 2 ;;
|
||||
esac
|
||||
|
||||
cd "$SKILL_DIR"
|
||||
install_python_tools
|
||||
install_opengrep
|
||||
install_powershell_modules
|
||||
if [[ $user_only -eq 1 ]]; then
|
||||
warn "--user-only: skipped system packages (gitleaks, osv-scanner, gh). Re-run without it to install them."
|
||||
else
|
||||
install_native
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Per-project tools (not installed here — must live in the target repo):
|
||||
- eslint + eslint-plugin-security (npm i -D)
|
||||
@@ -158,3 +202,9 @@ Per-project tools (not installed here — must live in the target repo):
|
||||
|
||||
Run /audit-code 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-code/runs.jsonl"
|
||||
|
||||
@@ -124,7 +124,7 @@ class Manifest:
|
||||
findings: list[Finding]
|
||||
package_diffs: dict[str, PackageDiff]
|
||||
tool_stats: dict[str, ToolStat]
|
||||
tools_unavailable: list[str]
|
||||
tools_unavailable: dict[str, str]
|
||||
errors: list[str]
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
@@ -138,7 +138,7 @@ class Manifest:
|
||||
"findings": [f.to_dict() for f in self.findings],
|
||||
"package_diffs": {k: v.to_dict() for k, v in self.package_diffs.items()},
|
||||
"tool_stats": {k: v.to_dict() for k, v in self.tool_stats.items()},
|
||||
"tools_unavailable": list(self.tools_unavailable),
|
||||
"tools_unavailable": dict(self.tools_unavailable),
|
||||
"errors": list(self.errors),
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def slice_for_agent(manifest: dict, agent: str) -> dict:
|
||||
"language_breakdown": manifest["language_breakdown"],
|
||||
"changed_files": list(manifest["changed_files"]),
|
||||
"tool_stats": dict(manifest["tool_stats"]),
|
||||
"tools_unavailable": list(manifest["tools_unavailable"]),
|
||||
"tools_unavailable": dict(manifest["tools_unavailable"]),
|
||||
"errors": list(manifest["errors"]),
|
||||
}
|
||||
findings = manifest["findings"]
|
||||
|
||||
Reference in New Issue
Block a user