Files
claude-plugin/plugins/reviews/skills/audit-terraform/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

185 lines
4.9 KiB
Python

"""Manifest dataclasses for collect-changes.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"]
Tool = Literal["terragrunt", "tofu"]
Action = Literal["create", "update", "delete", "replace", "read", "no-op", "unknown"]
Source = Literal["plan", "diff", "both"]
@dataclass
class InitResult:
ok: bool
stdout_tail: str
stderr_tail: str
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class PlanResult:
ok: bool
stdout_path: str
exit_code: int
summary: str
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class PlanUnit:
plan_dir: str
tool: Tool
init: InitResult
plan: PlanResult
triggered_by: list[str]
terragrunt_changed: bool = False
changed_files: list[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"plan_dir": self.plan_dir,
"tool": self.tool,
"init": self.init.to_dict(),
"plan": self.plan.to_dict(),
"triggered_by": list(self.triggered_by),
"terragrunt_changed": self.terragrunt_changed,
"changed_files": list(self.changed_files),
}
@dataclass
class TrivyFinding:
check_id: str
title: str
severity: str
message: str
file: str
start_line: int = 0
end_line: int = 0
resource_type: str = ""
source: str = "trivy"
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class TflintFinding:
rule: str
severity: str
message: str
file: str
start_line: int = 0
end_line: int = 0
link: str = ""
source: str = "tflint"
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class CatalogInstance:
plan_dir: str
address_at_plan: str
action: Action
def to_dict(self) -> dict:
return asdict(self)
@dataclass
class CatalogEntry:
source_dir: str
local_address: str
type: str
source: Source
instances: list[CatalogInstance] = field(default_factory=list)
block_header: str = ""
evidence_line: str = ""
key_attributes: dict[str, str | bool | int | list[str]] = field(default_factory=dict)
review_context: dict[str, object] = field(default_factory=dict)
block_file: str = ""
block_start: int = 0
block_end: int = 0
def to_dict(self) -> dict:
return {
"source_dir": self.source_dir,
"local_address": self.local_address,
"type": self.type,
"source": self.source,
"instances": [i.to_dict() for i in self.instances],
"block_header": self.block_header,
"evidence_line": self.evidence_line,
"key_attributes": dict(self.key_attributes),
"review_context": dict(self.review_context),
"block_file": self.block_file,
"block_start": self.block_start,
"block_end": self.block_end,
}
@dataclass
class ModuleGraphEntry:
callsites: list[str]
sibling_modules_at_callsites: list[str] = field(default_factory=list)
callsite_local_names: dict[str, dict[str, str]] = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"callsites": list(self.callsites),
"sibling_modules_at_callsites": list(self.sibling_modules_at_callsites),
"callsite_local_names": {
callsite: dict(local_names)
for callsite, local_names in self.callsite_local_names.items()
},
}
@dataclass
class Manifest:
base_ref: str
head_ref: str
mode: Mode
default_branch: str
changed_source_dirs: list[str]
plan_units: list[PlanUnit]
catalog: list[CatalogEntry]
trivy_findings: list[TrivyFinding]
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 {
"base_ref": self.base_ref,
"head_ref": self.head_ref,
"mode": self.mode,
"default_branch": self.default_branch,
"changed_source_dirs": list(self.changed_source_dirs),
"plan_units": [p.to_dict() for p in self.plan_units],
"catalog": [c.to_dict() for c in self.catalog],
"trivy_findings": [f.to_dict() for f in self.trivy_findings],
"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:
return json.dumps(self.to_dict(), indent=indent, sort_keys=False)