@intentsolutions/audit-harness 1.1.5 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ audit-harness fp-rate — measure a gate's false-positive / false-negative rate over
4
+ a labeled corpus.
5
+
6
+ A new gate ships `enforcement: advisory`. It earns promotion to `blocking` only
7
+ once its measured false-positive rate (clean inputs it wrongly flags) sits below a
8
+ stated bar on a labeled corpus. This harness produces that measurement — the
9
+ evidence an engineer cites when they pin `enforcement: blocking` in a repo's
10
+ `tests/TESTING.md` (PP-PLAN-040 Phase 0, bead c2e; rule: docs/gate-promotion.md).
11
+
12
+ Labeled corpus layout (default `tests/fixtures/conform`):
13
+ <corpus>/valid/<fixture>/... → every gate that fires here SHOULD be clean
14
+ <corpus>/malformed/<fixture>/... → every gate that fires here SHOULD flag
15
+
16
+ Per row the gate emits on a fixture, the verdict is bucketed:
17
+ clean = PASS | NOT_APPLICABLE
18
+ flag = FAIL | ADVISORY(advisory_severity=error)
19
+ skip = ADVISORY indeterminate (tool/schema absent) — unmeasurable, excluded
20
+
21
+ false positive (FP) = a `valid` fixture the gate flagged
22
+ false negative (FN) = a `malformed` fixture the gate left clean
23
+
24
+ Stdlib only. Read-only. Default exit 0 (report); `--max-fp-rate X` exits 1 if any
25
+ gate exceeds the bar (use in CI when promoting a gate).
26
+ """
27
+ import argparse
28
+ import json
29
+ import os
30
+ import subprocess
31
+ import sys
32
+
33
+ HERE = os.path.dirname(os.path.abspath(__file__))
34
+ CONFORM = os.path.join(HERE, "conform.py")
35
+ DEFAULT_CORPUS = os.path.join(HERE, "..", "tests", "fixtures", "conform")
36
+
37
+
38
+ def verdict_bucket(row):
39
+ r = row.get("result")
40
+ if r in ("PASS", "NOT_APPLICABLE"):
41
+ return "clean"
42
+ if r == "FAIL":
43
+ return "flag"
44
+ if r == "ADVISORY":
45
+ if row.get("metadata", {}).get("indeterminate"):
46
+ return "skip"
47
+ if row.get("advisory_severity") == "error":
48
+ return "flag"
49
+ return "skip"
50
+ return "skip"
51
+
52
+
53
+ def run_conform(fixture):
54
+ out = subprocess.run([sys.executable, CONFORM, fixture], capture_output=True, text=True)
55
+ try:
56
+ return json.loads(out.stdout)
57
+ except Exception:
58
+ return []
59
+
60
+
61
+ def measure(corpus):
62
+ # per gate_id: {valid_total, fp, malformed_total, fn, skipped}
63
+ stats = {}
64
+
65
+ def bump(gid, key):
66
+ s = stats.setdefault(gid, {"valid": 0, "fp": 0, "malformed": 0, "fn": 0, "skipped": 0})
67
+ s[key] += 1
68
+
69
+ for label in ("valid", "malformed"):
70
+ base = os.path.join(corpus, label)
71
+ if not os.path.isdir(base):
72
+ continue
73
+ for name in sorted(os.listdir(base)):
74
+ fixture = os.path.join(base, name)
75
+ if not os.path.isdir(fixture):
76
+ continue
77
+ for row in run_conform(fixture):
78
+ gid = row["gate_id"]
79
+ bucket = verdict_bucket(row)
80
+ if bucket == "skip":
81
+ bump(gid, "skipped")
82
+ continue
83
+ if label == "valid":
84
+ bump(gid, "valid")
85
+ if bucket == "flag":
86
+ bump(gid, "fp")
87
+ else:
88
+ bump(gid, "malformed")
89
+ if bucket == "clean":
90
+ bump(gid, "fn")
91
+ return stats
92
+
93
+
94
+ def rate(n, d):
95
+ return (n / d) if d else 0.0
96
+
97
+
98
+ def main():
99
+ ap = argparse.ArgumentParser(description="Measure gate FP/FN rate over a labeled corpus")
100
+ ap.add_argument("--corpus", default=DEFAULT_CORPUS, help="labeled corpus root (valid/ + malformed/)")
101
+ ap.add_argument("--json", action="store_true", help="emit JSON report to stdout")
102
+ ap.add_argument("--max-fp-rate", type=float, default=None,
103
+ help="exit 1 if any measured gate's FP-rate exceeds this (promotion gate)")
104
+ args = ap.parse_args()
105
+
106
+ corpus = os.path.abspath(args.corpus)
107
+ stats = measure(corpus)
108
+
109
+ report = {}
110
+ for gid, s in sorted(stats.items()):
111
+ report[gid] = {
112
+ "valid_samples": s["valid"],
113
+ "false_positives": s["fp"],
114
+ "fp_rate": round(rate(s["fp"], s["valid"]), 4),
115
+ "malformed_samples": s["malformed"],
116
+ "false_negatives": s["fn"],
117
+ "fn_rate": round(rate(s["fn"], s["malformed"]), 4),
118
+ "skipped_indeterminate": s["skipped"],
119
+ }
120
+
121
+ if args.json:
122
+ print(json.dumps({"corpus": os.path.relpath(corpus, os.path.join(HERE, "..")),
123
+ "gates": report}, indent=2))
124
+ else:
125
+ print(f"FP/FN rate over corpus: {os.path.relpath(corpus, os.path.join(HERE, '..'))}")
126
+ print(f"{'gate_id':<42} {'valid':>5} {'FP':>3} {'FP%':>6} {'malf':>4} {'FN':>3} {'FN%':>6}")
127
+ for gid, r in report.items():
128
+ print(f"{gid:<42} {r['valid_samples']:>5} {r['false_positives']:>3} "
129
+ f"{r['fp_rate']*100:>5.1f}% {r['malformed_samples']:>4} "
130
+ f"{r['false_negatives']:>3} {r['fn_rate']*100:>5.1f}%")
131
+ if not report:
132
+ print(" (no measurable gate verdicts in corpus)")
133
+
134
+ if args.max_fp_rate is not None:
135
+ over = {g: r["fp_rate"] for g, r in report.items() if r["fp_rate"] > args.max_fp_rate}
136
+ if over:
137
+ sys.stderr.write(f"\nfp-rate: {len(over)} gate(s) exceed --max-fp-rate={args.max_fp_rate}:\n")
138
+ for g, fr in over.items():
139
+ sys.stderr.write(f" {g}: {fr:.4f}\n")
140
+ sys.exit(1)
141
+ sys.exit(0)
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ audit-harness gen-layer-applicability — project the canonical registry datum into
4
+ the human-readable layer-applicability matrix.
5
+
6
+ `schemas/audit-profile/registry.v1.json` is THE single source of truth for "which
7
+ gates apply to repo-type X, in which dimension, at what applicability". This
8
+ generator renders `schemas/audit-profile/layer-applicability.md` as a PROJECTION
9
+ of that datum so the doc can never silently drift from the registry the classifier
10
+ actually resolves against (PP-PLAN-040 Phase 0, bead c2b).
11
+
12
+ Modes:
13
+ (default) print the rendered markdown to stdout
14
+ --write write it to schemas/audit-profile/layer-applicability.md
15
+ --check regenerate in-memory and diff against the committed file;
16
+ exit 1 on drift (the CI `layer-applicability-drift` gate)
17
+
18
+ Stdlib only. Read-only except in --write mode (which only writes the one doc).
19
+ """
20
+ import argparse
21
+ import difflib
22
+ import hashlib
23
+ import json
24
+ import os
25
+ import sys
26
+
27
+ HERE = os.path.dirname(os.path.abspath(__file__))
28
+ REGISTRY = os.path.join(HERE, "..", "schemas", "audit-profile", "registry.v1.json")
29
+ DOC = os.path.join(HERE, "..", "schemas", "audit-profile", "layer-applicability.md")
30
+
31
+ GLYPH = {"required": "✅", "recommended": "⭕", "conditional": "⚠", "waived": "❌"}
32
+
33
+
34
+ def sha256_file(path):
35
+ h = hashlib.sha256()
36
+ with open(path, "rb") as f:
37
+ for chunk in iter(lambda: f.read(65536), b""):
38
+ h.update(chunk)
39
+ return "sha256:" + h.hexdigest()
40
+
41
+
42
+ def row(gate):
43
+ app = gate.get("applicability", "")
44
+ return "| `{gid}` | {dim} | {glyph} {app} | {enf} | {tool} |".format(
45
+ gid=gate["gate_id"],
46
+ dim=gate.get("dimension", ""),
47
+ glyph=GLYPH.get(app, ""),
48
+ app=app,
49
+ enf=gate.get("enforcement", "advisory"),
50
+ tool=("`" + gate["tool"] + "`") if gate.get("tool") else "—",
51
+ )
52
+
53
+
54
+ def table(gates):
55
+ out = ["| Gate | Dimension | Applicability | Enforcement | Tool |",
56
+ "|---|---|---|---|---|"]
57
+ out += [row(g) for g in sorted(gates, key=lambda g: (g.get("dimension", ""), g["gate_id"]))]
58
+ return "\n".join(out)
59
+
60
+
61
+ def render(registry, registry_hash):
62
+ lines = []
63
+ a = lines.append
64
+ a("# Layer Applicability — GENERATED from `registry.v1.json`")
65
+ a("")
66
+ a("> ⚠️ **GENERATED FILE — do not edit by hand.**")
67
+ a("> Source of truth: [`registry.v1.json`](registry.v1.json) "
68
+ "(the canonical dimension→gate datum; `classify` resolves against it).")
69
+ a("> Regenerate: `audit-harness gen-layer-applicability --write` "
70
+ "(or `python3 scripts/gen-layer-applicability.py --write`).")
71
+ a("> CI gate `layer-applicability-drift` fails the build if this file drifts from the registry.")
72
+ a(">")
73
+ a(f"> registry `{registry_hash}`")
74
+ a("")
75
+ a(registry.get("description", "").strip())
76
+ a("")
77
+ a("**Legend (applicability):** "
78
+ + " · ".join(f"{GLYPH[k]} {k}" for k in ("required", "recommended", "conditional", "waived")))
79
+ a("")
80
+ a("Every gate defaults to `enforcement: advisory`. Blocking is **earned** — "
81
+ "engineer-pinned in the target repo's `tests/TESTING.md`, FP-rate-gated "
82
+ "(see [`gate-promotion.md`](../../docs/gate-promotion.md)).")
83
+ a("")
84
+ a("## Base gates (apply to every repo)")
85
+ a("")
86
+ a(table(registry.get("base", [])))
87
+ a("")
88
+ a("## By classification")
89
+ a("")
90
+ a("A repo carries the **UNION** of every classification it matches "
91
+ "(`classify` never picks a single winner). Gates dedup by `gate_id`, "
92
+ "keeping the highest applicability.")
93
+ a("")
94
+ for kind in sorted(registry.get("classifications", {})):
95
+ a(f"### `{kind}`")
96
+ a("")
97
+ a(table(registry["classifications"][kind]))
98
+ a("")
99
+ overlays = registry.get("overlays", {})
100
+ if overlays:
101
+ a("## Overlays")
102
+ a("")
103
+ for name in sorted(overlays):
104
+ ov = overlays[name]
105
+ a(f"### `{name}`")
106
+ a("")
107
+ a(ov.get("description", "").strip())
108
+ promote = ov.get("promote_to_required", [])
109
+ if promote:
110
+ a("")
111
+ a("Promotes to **required**: " + ", ".join(f"`{d}`" for d in promote) + ".")
112
+ a("")
113
+ return "\n".join(lines).rstrip() + "\n"
114
+
115
+
116
+ def main():
117
+ ap = argparse.ArgumentParser(description="Project registry.v1.json -> layer-applicability.md")
118
+ ap.add_argument("--write", action="store_true", help="write the doc to its canonical path")
119
+ ap.add_argument("--check", action="store_true", help="fail (exit 1) if the committed doc drifts")
120
+ ap.add_argument("--registry", default=REGISTRY)
121
+ ap.add_argument("--out", default=DOC)
122
+ args = ap.parse_args()
123
+
124
+ registry_path = os.path.abspath(args.registry)
125
+ with open(registry_path, "r", encoding="utf-8") as f:
126
+ registry = json.load(f)
127
+ rendered = render(registry, sha256_file(registry_path))
128
+
129
+ if args.check:
130
+ try:
131
+ with open(args.out, "r", encoding="utf-8") as f:
132
+ current = f.read()
133
+ except FileNotFoundError:
134
+ print(f"gen-layer-applicability: {args.out} missing — run --write", file=sys.stderr)
135
+ sys.exit(1)
136
+ if current != rendered:
137
+ diff = difflib.unified_diff(
138
+ current.splitlines(True), rendered.splitlines(True),
139
+ fromfile="committed", tofile="generated",
140
+ )
141
+ sys.stderr.write("".join(diff))
142
+ sys.stderr.write("\ngen-layer-applicability: DRIFT — regenerate with --write\n")
143
+ sys.exit(1)
144
+ print("gen-layer-applicability: layer-applicability.md matches the registry datum")
145
+ sys.exit(0)
146
+
147
+ if args.write:
148
+ with open(args.out, "w", encoding="utf-8") as f:
149
+ f.write(rendered)
150
+ print(f"gen-layer-applicability: wrote {os.path.relpath(args.out, os.path.join(HERE, '..'))}")
151
+ sys.exit(0)
152
+
153
+ sys.stdout.write(rendered)
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ audit-harness scan — read-only security / hygiene / skill-quality gate-runner
4
+ (PP-PLAN-040 Phase 4 / E6).
5
+
6
+ For every `dimension: security | hygiene | skill-quality` gate in a repo's
7
+ audit-profile/v1, scan runs the right external tool with the repo present and wraps
8
+ its exit code into a `gate-result/v1` row (JSON array, stdout). Advisory-first; a
9
+ missing tool degrades to ADVISORY indeterminate (never a false FAIL). It NEVER
10
+ fixes anything and NEVER reimplements a scanner.
11
+
12
+ Strategies:
13
+ - local hygiene-readme: deterministic README presence check (no tool).
14
+ - shell-out every gate carrying a `tool` (gitleaks, osv-scanner, semgrep, syft,
15
+ markdownlint, lychee, ...): run it if on PATH; clean exit -> PASS;
16
+ findings -> ADVISORY(error) (or FAIL under --strict / blocking);
17
+ tool absent -> ADVISORY indeterminate.
18
+ - consume skill-quality skill-behavioral (tool j-rig): CONSUME a j-rig
19
+ Evidence Bundle verdict row (--jrig-verdict PATH or a default
20
+ location). The harness does NOT run behavioral judgment itself —
21
+ it ingests j-rig's verdict. No verdict -> ADVISORY indeterminate.
22
+
23
+ Stdlib only. No network beyond whatever the shelled-out tool does (and the only
24
+ network-touching gates fail open to indeterminate). No filesystem mutation.
25
+ """
26
+ import argparse
27
+ import hashlib
28
+ import json
29
+ import os
30
+ import shutil
31
+ import subprocess
32
+ import sys
33
+ from datetime import datetime, timezone
34
+
35
+ HERE = os.path.dirname(os.path.abspath(__file__))
36
+ if HERE not in sys.path:
37
+ sys.path.insert(0, HERE)
38
+ import classify as C # noqa: E402
39
+
40
+ EMPTY_SHA = "sha256:" + hashlib.sha256(b"").hexdigest()
41
+ SCAN_DIMENSIONS = {"security", "hygiene", "skill-quality"}
42
+
43
+ # tool -> argv (run with cwd=repo). "generation" tools (syft) are PASS on exit 0,
44
+ # INDETERMINATE on failure (they produce an artifact, they don't pass/fail policy).
45
+ TOOL_CMD = {
46
+ "gitleaks": (["gitleaks", "detect", "--no-banner"], "scan"),
47
+ "osv-scanner": (["osv-scanner", "-r", "."], "scan"),
48
+ "semgrep": (["semgrep", "scan", "--error", "--quiet"], "scan"),
49
+ "syft": (["syft", "."], "generation"),
50
+ "markdownlint": (["markdownlint", "."], "scan"),
51
+ "lychee": (["lychee", "--offline", "--no-progress", "."], "scan"),
52
+ }
53
+
54
+
55
+ def sha256_str(s):
56
+ return "sha256:" + hashlib.sha256(s.encode("utf-8")).hexdigest()
57
+
58
+
59
+ def make_row(gate_id, result, *, policy_hash, input_hash, commit_sha, runner,
60
+ metadata=None, failure_mode=None, advisory_severity=None):
61
+ row = {
62
+ "gate_id": gate_id, "result": result, "policy_hash": policy_hash,
63
+ "input_hash": input_hash,
64
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
65
+ "runner": runner, "commit_sha": commit_sha,
66
+ }
67
+ if metadata:
68
+ row["metadata"] = metadata
69
+ if failure_mode is not None:
70
+ row["failure_mode"] = failure_mode
71
+ if advisory_severity is not None:
72
+ row["advisory_severity"] = advisory_severity
73
+ return row
74
+
75
+
76
+ def gate_suffix(gate_id):
77
+ return gate_id.rsplit(":", 1)[-1]
78
+
79
+
80
+ def indeterminate(gate, commit_sha, runner, reason, policy):
81
+ return make_row(gate["gate_id"], "ADVISORY", policy_hash=sha256_str(policy),
82
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
83
+ advisory_severity="warn",
84
+ metadata={"indeterminate": True, "reason": reason})
85
+
86
+
87
+ def run_readme(repo, gate, commit_sha, runner, strict):
88
+ enforcement = gate.get("enforcement", "advisory")
89
+ present = any(os.path.isfile(os.path.join(repo, n))
90
+ for n in ("README.md", "README.rst", "README.txt", "README"))
91
+ if present:
92
+ return make_row(gate["gate_id"], "PASS", policy_hash=sha256_str("hygiene:readme"),
93
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
94
+ metadata={"method": "local-presence", "signal": "README present"})
95
+ result, fm, sev = ("FAIL", "hygiene:readme-missing", None) if (strict or enforcement == "blocking") \
96
+ else ("ADVISORY", None, "warn")
97
+ return make_row(gate["gate_id"], result, policy_hash=sha256_str("hygiene:readme"),
98
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
99
+ failure_mode=fm, advisory_severity=sev,
100
+ metadata={"method": "local-presence", "reason": "no README found"})
101
+
102
+
103
+ def run_tool(tool, repo, gate, commit_sha, runner, strict):
104
+ enforcement = gate.get("enforcement", "advisory")
105
+ policy = f"tool:{tool}"
106
+ if tool not in TOOL_CMD:
107
+ return indeterminate(gate, commit_sha, runner,
108
+ f"no invocation wired for tool '{tool}'", policy)
109
+ if shutil.which(tool) is None:
110
+ return indeterminate(gate, commit_sha, runner,
111
+ f"{tool} not on PATH — {gate.get('dimension')} unmeasured", policy)
112
+ argv, kind = TOOL_CMD[tool]
113
+ try:
114
+ proc = subprocess.run(argv, cwd=repo, capture_output=True, text=True, timeout=300)
115
+ except Exception as e:
116
+ return indeterminate(gate, commit_sha, runner, f"{tool} failed to run: {e}", policy)
117
+ if proc.returncode == 0:
118
+ return make_row(gate["gate_id"], "PASS", policy_hash=sha256_str(policy),
119
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
120
+ metadata={"method": "shell-out", "tool": tool})
121
+ if kind == "generation":
122
+ # syft etc. failing to generate is infra, not a policy violation
123
+ return indeterminate(gate, commit_sha, runner,
124
+ f"{tool} could not generate artifact (exit {proc.returncode})", policy)
125
+ detail = (proc.stdout or proc.stderr).strip()[:2000]
126
+ result, fm, sev = ("FAIL", f"scan:{tool}-findings", None) if (strict or enforcement == "blocking") \
127
+ else ("ADVISORY", None, "error")
128
+ return make_row(gate["gate_id"], result, policy_hash=sha256_str(policy),
129
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
130
+ failure_mode=fm, advisory_severity=sev,
131
+ metadata={"method": "shell-out", "tool": tool, "detail": detail})
132
+
133
+
134
+ def consume_jrig(repo, gate, commit_sha, runner, strict, verdict_path):
135
+ """Ingest a j-rig Evidence Bundle verdict row — never run judgment here."""
136
+ policy = "consume:j-rig"
137
+ candidates = [verdict_path] if verdict_path else []
138
+ candidates += [os.path.join(repo, p) for p in
139
+ (".j-rig/verdict.json", ".jrig/verdict.json", "j-rig-verdict.json")]
140
+ path = next((p for p in candidates if p and os.path.isfile(p)), None)
141
+ if path is None:
142
+ return indeterminate(gate, commit_sha, runner,
143
+ "no j-rig verdict available — run j-rig eval and pass --jrig-verdict",
144
+ policy)
145
+ verdict = C.read_json(path)
146
+ if not isinstance(verdict, dict):
147
+ return indeterminate(gate, commit_sha, runner, f"unreadable j-rig verdict at {path}", policy)
148
+ # Pass through j-rig's own result if present; otherwise interpret a boolean pass.
149
+ enforcement = gate.get("enforcement", "advisory")
150
+ jres = verdict.get("result") or ("PASS" if verdict.get("passed") else "FAIL")
151
+ meta = {"method": "consume-j-rig", "source": os.path.relpath(path, repo),
152
+ "jrig": {k: verdict.get(k) for k in ("result", "passed", "layers_passed", "baseline_delta")
153
+ if k in verdict}}
154
+ if jres == "PASS":
155
+ return make_row(gate["gate_id"], "PASS", policy_hash=sha256_str(policy),
156
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner, metadata=meta)
157
+ result, fm, sev = ("FAIL", "skill-quality:jrig-fail", None) if (strict or enforcement == "blocking") \
158
+ else ("ADVISORY", None, "error")
159
+ return make_row(gate["gate_id"], result, policy_hash=sha256_str(policy),
160
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
161
+ failure_mode=fm, advisory_severity=sev, metadata=meta)
162
+
163
+
164
+ def compute_profile(repo, registry_path, profile_arg):
165
+ if profile_arg == "-":
166
+ return json.load(sys.stdin)
167
+ if profile_arg:
168
+ with open(profile_arg, "r", encoding="utf-8") as f:
169
+ return json.load(f)
170
+ out = subprocess.run([sys.executable, os.path.join(HERE, "classify.py"), repo,
171
+ "--registry", registry_path], capture_output=True, text=True)
172
+ if out.returncode != 0:
173
+ sys.stderr.write(out.stderr)
174
+ raise SystemExit(2)
175
+ return json.loads(out.stdout)
176
+
177
+
178
+ def main():
179
+ ap = argparse.ArgumentParser(description="Security/hygiene/skill-quality gate-runner -> gate-result/v1")
180
+ ap.add_argument("repo", nargs="?", default=".")
181
+ ap.add_argument("--strict", action="store_true", help="treat a finding/gap as FAIL (exit 1)")
182
+ ap.add_argument("--registry", default=C.DEFAULT_REGISTRY)
183
+ ap.add_argument("--profile", default=None, help="pinned audit-profile/v1 (PATH or '-')")
184
+ ap.add_argument("--jrig-verdict", default=None, help="path to a j-rig Evidence Bundle verdict to consume")
185
+ args = ap.parse_args()
186
+
187
+ repo = os.path.abspath(args.repo)
188
+ runner = f"audit-harness@{C.harness_version()}"
189
+
190
+ override_path = os.path.join(repo, ".audit-harness.yml")
191
+ override = C.parse_override(override_path) if os.path.isfile(override_path) else {"disable": False}
192
+ if override.get("disable") or os.environ.get("AUDIT_HARNESS_DISABLE") == "1":
193
+ sys.stderr.write("audit-harness: KILL-SWITCH active — scan skipped (no rows emitted)\n")
194
+ print("[]")
195
+ sys.exit(0)
196
+
197
+ profile = compute_profile(repo, os.path.abspath(args.registry), args.profile)
198
+ commit_sha = profile.get("subject", {}).get("commit_sha") or C.git_short_sha(repo)
199
+
200
+ gates = [g for g in profile.get("gates", [])
201
+ if g.get("dimension") in SCAN_DIMENSIONS and g.get("enforcement") != "disabled"]
202
+
203
+ rows = []
204
+ for gate in gates:
205
+ suffix = gate_suffix(gate["gate_id"])
206
+ tool = gate.get("tool")
207
+ if suffix == "hygiene-readme":
208
+ rows.append(run_readme(repo, gate, commit_sha, runner, args.strict))
209
+ elif tool == "j-rig":
210
+ rows.append(consume_jrig(repo, gate, commit_sha, runner, args.strict, args.jrig_verdict))
211
+ elif tool:
212
+ rows.append(run_tool(tool, repo, gate, commit_sha, runner, args.strict))
213
+ else:
214
+ rows.append(indeterminate(gate, commit_sha, runner,
215
+ f"gate '{suffix}' has no tool wired in this harness version",
216
+ f"scan:{suffix}"))
217
+
218
+ print(json.dumps(rows, indent=2))
219
+ n_fail = sum(1 for r in rows if r["result"] == "FAIL")
220
+ n_adv = sum(1 for r in rows if r["result"] == "ADVISORY")
221
+ n_pass = sum(1 for r in rows if r["result"] == "PASS")
222
+ sys.stderr.write(f"audit-harness scan: {n_pass} PASS, {n_adv} ADVISORY, {n_fail} FAIL "
223
+ f"across {len(rows)} gate(s)\n")
224
+ sys.exit(1 if n_fail else 0)
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()