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.
147 lines
3.6 KiB
Python
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)
|