94 lines
5.2 KiB
Python
94 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Lexically audit known runtime safety hazards without executing project code."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
RULES = (
|
|
("BLOCK", "AUTO_DEPENDENCY_INSTALL", re.compile(r"\bpip\s+install\b"), ("tools/*.sh",)),
|
|
("BLOCK", "REMOTE_MODEL_RESOLUTION", re.compile(r"\bget_pretrained\s*\("), ("tools/*.py",)),
|
|
("BLOCK", "NAMED_YOLO_ACQUISITION", re.compile(r"(?:LOCAL_CV_YOLO_MODEL.*:-|os\.getenv\([^,]+,)\s*[\"']yolo[^\"']*\.pt|YOLO\([^\n]*LOCAL_CV_YOLO_MODEL"), ("tools/*.sh", "tools/*.py")),
|
|
("WARN", "YOLO_LOAD_REQUIRES_REVIEW", re.compile(r"\bYOLO\s*\("), ("tools/*.py",)),
|
|
("BLOCK", "PLACEHOLDER_TONE", re.compile(r"fallbackTone|fallback-tone|ffmpeg-sine"), ("src/main/java/**/*.java",)),
|
|
("BLOCK", "PLACEHOLDER_TONE", re.compile(r"write_fallback_tone\s*\("), ("tools/*.py",)),
|
|
("BLOCK", "PLACEHOLDER_SILENCE", re.compile(r"writeSilence|strategy=silence|anullsrc="), ("src/main/java/**/*.java",)),
|
|
("BLOCK", "PLACEHOLDER_SILENCE", re.compile(r"write_silence\s*\("), ("tools/*.py",)),
|
|
("BLOCK", "AUTO_START_DEFAULT_TRUE", re.compile(r"auto-start:\s*\$\{[^}:]+:true\}"), ("src/main/resources/application*.yml",)),
|
|
("BLOCK", "RENDER_DEFAULT_TRUE", re.compile(r"render-enabled:\s*\$\{[^}:]+:true\}"), ("src/main/resources/application*.yml",)),
|
|
("BLOCK", "APPROVAL_DEFAULT_FALSE", re.compile(r"require-director-approval:\s*\$\{[^}:]+:false\}"), ("src/main/resources/application*.yml",)),
|
|
("BLOCK", "HEURISTIC_FALLBACK_DEFAULT_TRUE", re.compile(r"fallback-to-heuristic:\s*\$\{[^}:]+:true\}"), ("src/main/resources/application*.yml",)),
|
|
("WARN", "CONFIGURED_HTTP_NON_LOOPBACK", re.compile(r"https?://(?!127\.0\.0\.1|localhost)", re.IGNORECASE), ("src/main/resources/application*",)),
|
|
)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Read-only lexical audit for runtime hazards; findings require human classification and a clean result is not compliance proof."
|
|
)
|
|
parser.add_argument("--repo", type=Path, default=Path("."), help="Repository root (default: current directory).")
|
|
parser.add_argument("--format", choices=("text", "json"), default="text")
|
|
parser.add_argument(
|
|
"--fail-on", choices=("block", "warn", "never"), default="block",
|
|
help="Exit 2 on BLOCK findings, on BLOCK or WARN findings, or never because of findings (default: block).",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def paths_for(repo: Path, globs: tuple[str, ...]) -> list[Path]:
|
|
found: set[Path] = set()
|
|
for pattern in globs:
|
|
found.update(path for path in repo.glob(pattern) if path.is_file())
|
|
return sorted(found, key=lambda path: str(path.relative_to(repo)))
|
|
|
|
|
|
def main() -> int:
|
|
opts = parse_args()
|
|
repo = opts.repo.resolve()
|
|
if not (repo / "pom.xml").is_file():
|
|
print(f"error: not a recognized repository root (pom.xml missing): {repo}", file=sys.stderr)
|
|
return 3
|
|
findings: list[dict] = []
|
|
try:
|
|
for severity, code, pattern, globs in RULES:
|
|
for path in paths_for(repo, globs):
|
|
for number, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
|
|
if pattern.search(line):
|
|
findings.append({"severity": severity, "code": code,
|
|
"path": str(path.relative_to(repo)), "line": number,
|
|
"evidence": line.strip()[:240]})
|
|
asset_root = repo / "input" / "highlights" / "assets"
|
|
if asset_root.is_dir():
|
|
license_markers = list(asset_root.glob("LICENSE*")) + list(asset_root.glob("**/*.license*"))
|
|
media = [p for p in asset_root.rglob("*") if p.is_file() and p.suffix.lower() in {".wav", ".mp3", ".flac", ".aif", ".aiff", ".ttf", ".otf", ".cube"}]
|
|
if media and not license_markers:
|
|
findings.append({"severity": "WARN", "code": "ASSET_LICENSE_MARKER_MISSING",
|
|
"path": str(asset_root.relative_to(repo)), "line": 0,
|
|
"evidence": f"{len(media)} media/font/LUT files and no LICENSE* or *.license* marker; directory membership and marker files are not license proof"})
|
|
except OSError as exc:
|
|
print(f"error:goal f {exc}", file=sys.stderr)
|
|
return 3
|
|
findings.sort(key=lambda item: (item["path"], item["line"], item["code"]))
|
|
if opts.format == "json":
|
|
print(json.dumps({"repo": str(repo), "findings": findings}, indent=2, sort_keys=True))
|
|
else:
|
|
for item in findings:
|
|
location = f"{item['path']}:{item['line']}" if item["line"] else item["path"]
|
|
print(f"{item['severity']} {item['code']} {location} {item['evidence']}")
|
|
print(f"SUMMARY block={sum(f['severity'] == 'BLOCK' for f in findings)} warn={sum(f['severity'] == 'WARN' for f in findings)}")
|
|
severities = {item["severity"] for item in findings}
|
|
if opts.fail_on == "block" and "BLOCK" in severities:
|
|
return 2
|
|
if opts.fail_on == "warn" and severities & {"BLOCK", "WARN"}:
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|