Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import time | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| AUDIT_DIR = ROOT / "audit" | |
| BASE_URL = os.environ.get("KINK_AUDIT_BASE_URL", "http://127.0.0.1:8011") | |
| HEADER_URL = BASE_URL if BASE_URL.endswith("/") else BASE_URL + "/" | |
| def run_step(name: str, cmd: list[str], artifact_dir: Path, env: dict[str, str] | None = None, optional: bool = False) -> dict: | |
| started = time.time() | |
| merged_env = os.environ.copy() | |
| if env: | |
| merged_env.update(env) | |
| result = subprocess.run(cmd, cwd=ROOT, env=merged_env, capture_output=True, text=True) | |
| step = { | |
| "name": name, | |
| "command": cmd, | |
| "ok": result.returncode == 0, | |
| "optional": optional, | |
| "elapsed_ms": round((time.time() - started) * 1000, 1), | |
| "stdout_path": None, | |
| "stderr_path": None, | |
| "returncode": result.returncode, | |
| } | |
| stdout_path = artifact_dir / f"{name}.stdout.txt" | |
| stderr_path = artifact_dir / f"{name}.stderr.txt" | |
| stdout_path.write_text(result.stdout) | |
| stderr_path.write_text(result.stderr) | |
| step["stdout_path"] = str(stdout_path) | |
| step["stderr_path"] = str(stderr_path) | |
| step["status"] = "passed" if step["ok"] else ("skipped_or_failed_optional" if optional else "failed") | |
| return step | |
| def which(path: str) -> str | None: | |
| return shutil.which(path) | |
| def header_probe() -> dict: | |
| proc = subprocess.run(["curl", "--max-time", "10", "-sD", "-", HEADER_URL, "-o", "/dev/null"], cwd=ROOT, capture_output=True, text=True, check=False) | |
| headers = {} | |
| for line in proc.stdout.splitlines(): | |
| if ":" in line: | |
| key, value = line.split(":", 1) | |
| headers[key.strip().lower()] = value.strip() | |
| issues = [] | |
| if "cache-control" not in headers: | |
| issues.append("missing_cache_control") | |
| if "content-security-policy" not in headers: | |
| issues.append("missing_csp") | |
| if "x-frame-options" not in headers: | |
| issues.append("missing_x_frame_options") | |
| return {"headers": headers, "issues": issues, "ok": not issues} | |
| def build_steps(tier: str, artifact_dir: Path) -> list[dict]: | |
| steps: list[dict] = [] | |
| env = {"KINK_AUDIT_ARTIFACT_DIR": str(artifact_dir), "KINK_AUDIT_BASE_URL": BASE_URL} | |
| python = sys.executable | |
| core = [ | |
| ("diagnose_ui_flow", [python, "scripts/diagnose_ui_flow.py"], False), | |
| ("ui_visual_audit", [python, "scripts/ui_visual_audit.py"], False), | |
| ("regression_checks", [python, "scripts/regression_checks.py"], False), | |
| ("audit_data_quality", [python, "scripts/audit_data_quality.py"], False), | |
| ] | |
| if tier == "deep": | |
| core.append(("record_product_flows", [python, "scripts/record_product_flows.py"], False)) | |
| for name, cmd, optional in core: | |
| steps.append(run_step(name, cmd, artifact_dir, env=env, optional=optional)) | |
| header_report = header_probe() | |
| steps.append({ | |
| "name": "security_headers", | |
| "status": "passed" if header_report["ok"] else "failed", | |
| "ok": header_report["ok"], | |
| "optional": False, | |
| "report": header_report, | |
| }) | |
| if which("npm"): | |
| for name, cmd, optional in [ | |
| ("axe_core", ["npm", "--prefix", str(AUDIT_DIR), "run", "axe"], False), | |
| ("lhci", ["npm", "--prefix", str(AUDIT_DIR), "run", "lhci"], False), | |
| ]: | |
| steps.append(run_step(name, cmd, artifact_dir, env=env, optional=optional)) | |
| if which("bandit"): | |
| steps.append( | |
| run_step( | |
| "bandit", | |
| [ | |
| "bandit", | |
| "-c", | |
| str(AUDIT_DIR / "config" / "bandit.yaml"), | |
| "-r", | |
| str(ROOT / "api.py"), | |
| str(ROOT / "backend"), | |
| ], | |
| artifact_dir, | |
| optional=False, | |
| ) | |
| ) | |
| if which("pip-audit"): | |
| steps.append(run_step("pip_audit", ["pip-audit"], artifact_dir, optional=False)) | |
| if which("semgrep"): | |
| steps.append( | |
| run_step( | |
| "semgrep", | |
| [ | |
| "semgrep", | |
| "--config", | |
| str(AUDIT_DIR / "config" / "semgrep.yml"), | |
| "--exclude", | |
| "node_modules", | |
| "--exclude", | |
| ".venv", | |
| "--exclude", | |
| ".cursor", | |
| "--exclude", | |
| "data", | |
| "--exclude", | |
| "frontend/dist", | |
| str(ROOT), | |
| ], | |
| artifact_dir, | |
| optional=False, | |
| ) | |
| ) | |
| if which("schemathesis"): | |
| steps.append(run_step( | |
| "schemathesis", | |
| [ | |
| "schemathesis", "run", f"{BASE_URL}/openapi.json", | |
| "--phases", "coverage", | |
| "--checks", "not_a_server_error,status_code_conformance,content_type_conformance,response_headers_conformance,response_schema_conformance,negative_data_rejection,unsupported_method", | |
| "--include-path-regex", r"^/$|^/health$|^/stats$|^/users$", | |
| "--max-failures=10", | |
| "--request-timeout", "5", | |
| "--max-examples", "3", | |
| "--generation-allow-x00=false", | |
| "--workers", "1", | |
| ], | |
| artifact_dir, | |
| optional=False, | |
| )) | |
| return steps | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description="Run fast or deep audits for the kink CLI web app.") | |
| parser.add_argument("--tier", choices=["fast", "deep"], default="fast") | |
| args = parser.parse_args() | |
| artifact_dir = Path("/tmp/kink_audit") / f"{args.tier}_{int(time.time())}" | |
| artifact_dir.mkdir(parents=True, exist_ok=True) | |
| steps = build_steps(args.tier, artifact_dir) | |
| summary = { | |
| "tier": args.tier, | |
| "base_url": BASE_URL, | |
| "artifact_dir": str(artifact_dir), | |
| "generated_at": time.time(), | |
| "steps": steps, | |
| } | |
| manifest = artifact_dir / "manifest.json" | |
| manifest.write_text(json.dumps(summary, indent=2)) | |
| print(json.dumps({"artifact_dir": str(artifact_dir), "manifest": str(manifest)}, indent=2)) | |
| failures = [step for step in steps if not step.get("ok") and not step.get("optional")] | |
| return 1 if failures else 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |