from __future__ import annotations import json import shutil import subprocess from pathlib import Path from typing import Any from scripts.manifest import TflintFinding, TrivyFinding def _normalize_trivy_findings(payload: dict[str, Any]) -> list[TrivyFinding]: findings: list[TrivyFinding] = [] for result in payload.get("Results", []): for misconf in result.get("Misconfigurations", []): cause = misconf.get("CauseMetadata") or {} resource = cause.get("Resource") or "" resource_type = "" if isinstance(resource, str) and "." in resource: resource_type = resource.split(".", 1)[0] findings.append(TrivyFinding( check_id=( misconf.get("AVDID") or misconf.get("ID") or misconf.get("Query") or "" ), title=misconf.get("Title") or misconf.get("ID") or "", severity=str(misconf.get("Severity") or "UNKNOWN").lower(), message=misconf.get("Message") or misconf.get("Description") or "", file=result.get("Target") or "", start_line=int(cause.get("StartLine") or 0), end_line=int(cause.get("EndLine") or 0), resource_type=resource_type, )) return findings def _run_trivy_config(repo: Path, changed_files_in_repo: list[str]) -> tuple[dict[str, Any], list[TrivyFinding], str | None]: changed_dirs = sorted({ str(Path(path).parent) for path in changed_files_in_repo }) scan_paths = [str((repo / rel).resolve()) for rel in changed_dirs] or [str(repo)] result = subprocess.run( ["trivy", "config", "--quiet", "--format", "json", *scan_paths], capture_output=True, text=True, check=False, ) if result.returncode != 0: stderr = result.stderr.strip() or result.stdout.strip() return {}, [], f"trivy config failed: {stderr}" try: payload = json.loads(result.stdout) if result.stdout.strip() else {} except json.JSONDecodeError as exc: return {}, [], f"trivy config output was not valid JSON: {exc}" return payload, _normalize_trivy_findings(payload), None def _normalize_tflint_findings(payload: dict, *, scanned_dir: str) -> list[TflintFinding]: findings: list[TflintFinding] = [] for issue in payload.get("issues", []) or []: rule = issue.get("rule", {}) or {} rng = issue.get("range", {}) or {} start = rng.get("start", {}) or {} end = rng.get("end", {}) or {} rel_file = rng.get("filename") or "" if "/" in rel_file: full = rel_file elif rel_file: full = (Path(scanned_dir) / rel_file).as_posix() else: full = "" findings.append(TflintFinding( rule=rule.get("name") or "", severity=str(rule.get("severity") or "warning").lower(), message=issue.get("message") or "", file=full, start_line=int(start.get("line") or 0), end_line=int(end.get("line") or 0), link=rule.get("link") or "", )) return findings def _run_tflint(repo: Path, changed_files_in_repo: list[str]) -> tuple[dict, list[TflintFinding], str | None]: if shutil.which("tflint") is None: return {}, [], None changed_dirs = sorted({ str(Path(p).parent.as_posix()) for p in changed_files_in_repo if p.endswith((".tf", ".tf.json")) }) if not changed_dirs: return {}, [], None aggregated: list[TflintFinding] = [] raw_payloads: dict[str, dict] = {} errs: list[str] = [] for rel in changed_dirs: target = (repo / rel).resolve() if not target.is_dir(): continue proc = subprocess.run( ["tflint", "--format", "json", "--chdir", str(target)], capture_output=True, text=True, check=False, ) if proc.returncode not in (0, 2): errs.append(f"tflint failed in {rel}: {proc.stderr.strip() or proc.stdout.strip()}") continue try: payload = json.loads(proc.stdout) if proc.stdout.strip() else {} except json.JSONDecodeError as exc: errs.append(f"tflint output in {rel} was not valid JSON: {exc}") continue raw_payloads[rel] = payload aggregated.extend(_normalize_tflint_findings(payload, scanned_dir=rel)) return {"by_dir": raw_payloads}, aggregated, ("; ".join(errs) if errs else None)