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.
185 lines
4.9 KiB
Python
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)
|