Files
claude-plugin/plugins/reviews/skills/audit-code/scripts/manifest.py
T
mroberts 37fc3fb291 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.
2026-09-22 15:21:28 -05:00

147 lines
3.6 KiB
Python

"""Manifest dataclasses for collect-findings.py output.
The manifest is the contract between the script and the review subagents.
Schema mirrors DESIGN.md.
"""
from __future__ import annotations
import json
from dataclasses import dataclass, field, asdict
from typing import Literal
Mode = Literal["local", "ref"]
Severity = Literal["critical", "high", "medium", "low", "info"]
Language = Literal[
"python", "javascript", "typescript", "csharp", "lua", "powershell",
"github-actions",
]
@dataclass
class Finding:
tool: str
rule_id: str
severity: Severity
file: str
line: int
end_line: int
message: str
cwe: str | None = None
fix_suggestion: str | None = None
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class ChangedFile:
path: str
language: str
added_lines: list[tuple[int, int]]
def to_dict(self) -> dict:
return {
"path": self.path,
"language": self.language,
"added_lines": [list(r) for r in self.added_lines],
}
@dataclass
class LanguageBreakdown:
python: int = 0
javascript: int = 0
typescript: int = 0
csharp: int = 0
lua: int = 0
powershell: int = 0
github_actions: int = 0
skipped_files: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class PackageEntry:
name: str
version: str
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class PackageUpgrade:
name: str
from_version: str
to_version: str
def to_dict(self) -> dict:
return {"name": self.name, "from": self.from_version, "to": self.to_version}
@dataclass
class PackageDiff:
added: list[PackageEntry] = field(default_factory=list)
removed: list[PackageEntry] = field(default_factory=list)
upgraded: list[PackageUpgrade] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"added": [p.to_dict() for p in self.added],
"removed": [p.to_dict() for p in self.removed],
"upgraded": [u.to_dict() for u in self.upgraded],
}
@dataclass
class ToolStat:
ran: bool
pre_filter: int = 0
post_filter: int = 0
reason: str | None = None
def to_dict(self) -> dict:
d: dict = {"ran": self.ran}
if self.ran:
d["pre_filter"] = self.pre_filter
d["post_filter"] = self.post_filter
else:
d["reason"] = self.reason or ""
return d
@dataclass
class Manifest:
mode: Mode
base_ref: str
head_ref: str
default_branch: str
language_breakdown: LanguageBreakdown
changed_files: list[ChangedFile]
findings: list[Finding]
package_diffs: dict[str, PackageDiff]
tool_stats: dict[str, ToolStat]
tools_unavailable: dict[str, str]
errors: list[str]
def to_dict(self) -> dict:
return {
"mode": self.mode,
"base_ref": self.base_ref,
"head_ref": self.head_ref,
"default_branch": self.default_branch,
"language_breakdown": self.language_breakdown.to_dict(),
"changed_files": [c.to_dict() for c in self.changed_files],
"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": dict(self.tools_unavailable),
"errors": list(self.errors),
}
def to_json(self, indent: int = 2) -> str:
return json.dumps(self.to_dict(), indent=indent, sort_keys=False)