@intentsolutions/audit-harness 1.1.5 → 1.1.7

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,386 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ audit-harness audit — read-only testing-depth gate-runner (PP-PLAN-040 Phase 3 / E5).
4
+
5
+ For every `dimension: testing-depth` gate in a repo's audit-profile/v1, audit
6
+ assesses the gate and emits a `gate-result/v1` row (JSON array, stdout). It is the
7
+ "finish the pyramid" diagnostic: it reports which testing-depth LAYERS a repo has
8
+ infrastructure for, advisory-first.
9
+
10
+ Two assessment strategies, both read-only:
11
+ - crap-score -> runs the bundled `crap` scorer (static complexity x coverage).
12
+ - presence -> a per-layer static heuristic (test dirs, framework configs,
13
+ dependency markers). Layer infra present -> PASS; absent ->
14
+ ADVISORY(warn) "testing-depth gap"; unknowable statically ->
15
+ ADVISORY indeterminate.
16
+
17
+ What audit deliberately does NOT do: execute the repo's test suite. Running
18
+ arbitrary, untrusted test suites is the job of the repo's own CI test step — the
19
+ harness wraps that step's verdict into Evidence, it does not replace it. audit
20
+ reports COVERAGE PRESENCE; execution stays in CI. Each row records its
21
+ `metadata.method` so the assessment provenance is explicit.
22
+
23
+ --fast (default): presence heuristics only (<10s on a reference repo).
24
+ --deep: presence + crap-score.
25
+ --strict: a testing-depth gap on an `enforcement: blocking` gate -> FAIL.
26
+
27
+ Stdlib only. No network. No filesystem mutation.
28
+ """
29
+ import argparse
30
+ import hashlib
31
+ import json
32
+ import os
33
+ import subprocess
34
+ import sys
35
+ from datetime import datetime, timezone
36
+
37
+ HERE = os.path.dirname(os.path.abspath(__file__))
38
+ if HERE not in sys.path:
39
+ sys.path.insert(0, HERE)
40
+ import classify as C # noqa: E402
41
+
42
+ EMPTY_SHA = "sha256:" + hashlib.sha256(b"").hexdigest()
43
+
44
+ SKIP_DIRS = ("node_modules", ".git", ".venv", "dist", "build", "vendor", "target")
45
+
46
+ # Gates assessed quickly (presence heuristics) belong to the fast tier; crap-score
47
+ # is deep-only because it shells out to radon/gocyclo and can be slow.
48
+ DEEP_ONLY = {"crap-score"}
49
+
50
+
51
+ def sha256_str(s):
52
+ return "sha256:" + hashlib.sha256(s.encode("utf-8")).hexdigest()
53
+
54
+
55
+ # --------------------------------------------------------------------------- #
56
+ # repo signal collectors
57
+ # --------------------------------------------------------------------------- #
58
+ def collect_node_deps(repo):
59
+ deps = {}
60
+ for pkgpath in [os.path.join(repo, "package.json")] + [
61
+ os.path.join(s, "package.json") for s in C.list_pkg_subdirs(repo)
62
+ ]:
63
+ pkg = C.read_json(pkgpath)
64
+ if isinstance(pkg, dict):
65
+ for k in ("dependencies", "devDependencies"):
66
+ if isinstance(pkg.get(k), dict):
67
+ deps.update(pkg[k])
68
+ return deps
69
+
70
+
71
+ def node_test_script(repo):
72
+ pkg = C.read_json(os.path.join(repo, "package.json")) or {}
73
+ scripts = pkg.get("scripts") if isinstance(pkg, dict) else None
74
+ return isinstance(scripts, dict) and bool(scripts.get("test"))
75
+
76
+
77
+ def py_dep_text(repo):
78
+ txt = ""
79
+ for f in ("requirements.txt", "pyproject.toml", "Pipfile", "setup.cfg", "tox.ini"):
80
+ p = os.path.join(repo, f)
81
+ if os.path.isfile(p):
82
+ try:
83
+ txt += open(p, "r", encoding="utf-8").read().lower()
84
+ except Exception:
85
+ pass
86
+ return txt
87
+
88
+
89
+ def walk_names(repo, max_depth=4):
90
+ """Yield (dirpath_rel, dirnames, filenames) skipping vendor/build dirs."""
91
+ repo = os.path.abspath(repo)
92
+ for root, dirs, files in os.walk(repo):
93
+ dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
94
+ depth = root[len(repo):].count(os.sep)
95
+ if depth > max_depth:
96
+ dirs[:] = []
97
+ continue
98
+ yield root, dirs, files
99
+
100
+
101
+ def has_dir(repo, *names):
102
+ targets = set(names)
103
+ for _root, dirs, _files in walk_names(repo):
104
+ if targets & set(dirs):
105
+ return True
106
+ return False
107
+
108
+
109
+ def has_file_matching(repo, predicate):
110
+ for _root, _dirs, files in walk_names(repo):
111
+ if any(predicate(f) for f in files):
112
+ return True
113
+ return False
114
+
115
+
116
+ def has_glob_suffix(repo, *suffixes):
117
+ return has_file_matching(repo, lambda f: any(f.endswith(s) for s in suffixes))
118
+
119
+
120
+ # --------------------------------------------------------------------------- #
121
+ # per-layer presence detectors -> (present: bool|None, signal: str)
122
+ # present True -> infra detected (PASS)
123
+ # present False -> no infra detected (ADVISORY gap)
124
+ # present None -> not assessable statically (ADVISORY indeterminate)
125
+ # --------------------------------------------------------------------------- #
126
+ def d_unit(repo, deps):
127
+ if node_test_script(repo):
128
+ return True, "package.json scripts.test"
129
+ if any(x in deps for x in ("vitest", "jest", "mocha", "ava", "@jest/core", "node:test")):
130
+ return True, "node test framework dep"
131
+ txt = py_dep_text(repo)
132
+ if any(x in txt for x in ("pytest", "unittest", "nose")):
133
+ return True, "python test framework"
134
+ if has_glob_suffix(repo, "_test.go"):
135
+ return True, "go *_test.go"
136
+ if has_dir(repo, "tests", "test", "__tests__") or \
137
+ has_glob_suffix(repo, ".test.ts", ".test.js", ".spec.ts", ".spec.js"):
138
+ return True, "test dir / *.test|spec file"
139
+ if has_file_matching(repo, lambda f: f.startswith("test_") and f.endswith(".py")):
140
+ return True, "python test_*.py"
141
+ return False, "no unit test infrastructure detected"
142
+
143
+
144
+ def d_integration(repo, deps):
145
+ if has_dir(repo, "integration") or \
146
+ has_glob_suffix(repo, ".integration.test.ts", ".integration.test.js", ".int.test.ts"):
147
+ return True, "integration test dir/files"
148
+ if any(x in deps for x in ("testcontainers", "supertest")):
149
+ return True, "integration tooling dep"
150
+ if "tests/integration" in py_dep_text(repo):
151
+ return True, "python integration tests"
152
+ return False, "no integration test infrastructure detected"
153
+
154
+
155
+ def d_e2e(repo, deps):
156
+ if any(x in deps for x in ("@playwright/test", "playwright", "cypress", "puppeteer", "@testing-library/react")):
157
+ return True, "e2e framework dep"
158
+ cfgs = ("playwright.config.ts", "cypress.config.ts", "cypress.config.js")
159
+ if has_dir(repo, "e2e") or any(os.path.isfile(os.path.join(repo, c)) for c in cfgs):
160
+ return True, "e2e config/dir"
161
+ return False, "no e2e test infrastructure detected"
162
+
163
+
164
+ def d_smoke(repo, deps):
165
+ pkg = C.read_json(os.path.join(repo, "package.json")) or {}
166
+ scripts = pkg.get("scripts") if isinstance(pkg, dict) else {}
167
+ if isinstance(scripts, dict) and any("smoke" in k for k in scripts):
168
+ return True, "package.json smoke script"
169
+ if has_dir(repo, "smoke") or has_file_matching(repo, lambda f: "smoke" in f.lower()):
170
+ return True, "smoke test dir/file"
171
+ return False, "no smoke test detected"
172
+
173
+
174
+ def d_perf(repo, deps):
175
+ if any(x in deps for x in ("benchmark", "tinybench", "vitest-bench", "k6", "autocannon")):
176
+ return True, "perf/bench dep"
177
+ if has_dir(repo, "bench", "benchmark", "benchmarks", "perf") or \
178
+ has_glob_suffix(repo, ".bench.ts", ".bench.js", "_bench.go"):
179
+ return True, "bench dir/files"
180
+ return False, "no performance test infrastructure detected"
181
+
182
+
183
+ def d_a11y(repo, deps):
184
+ if any(x in deps for x in ("axe-core", "@axe-core/playwright", "jest-axe", "pa11y")):
185
+ return True, "a11y tooling dep"
186
+ return False, "no accessibility test infrastructure detected"
187
+
188
+
189
+ def d_contract(repo, deps):
190
+ if any(x in deps for x in ("@pact-foundation/pact", "pact")):
191
+ return True, "contract testing dep (pact)"
192
+ if has_dir(repo, "contract", "contracts", "pacts"):
193
+ return True, "contract test dir"
194
+ return False, "no contract test infrastructure detected"
195
+
196
+
197
+ def d_migration(repo, deps):
198
+ if has_dir(repo, "migrations", "migration"):
199
+ return True, "migrations dir"
200
+ if any(x in deps for x in ("prisma", "knex", "typeorm", "drizzle-kit")):
201
+ return True, "migration tooling dep"
202
+ if any(x in py_dep_text(repo) for x in ("alembic", "django")):
203
+ return True, "python migration tooling"
204
+ return False, "no migration test infrastructure detected"
205
+
206
+
207
+ def d_property(repo, deps):
208
+ if any(x in deps for x in ("fast-check", "jsverify")):
209
+ return True, "property-based dep (fast-check)"
210
+ if any(x in py_dep_text(repo) for x in ("hypothesis",)):
211
+ return True, "python hypothesis"
212
+ if "proptest" in py_dep_text(repo) or has_glob_suffix(repo, "_proptest.rs"):
213
+ return True, "rust proptest"
214
+ return False, "no property-based test infrastructure detected"
215
+
216
+
217
+ def d_fuzz(repo, deps):
218
+ if has_dir(repo, "fuzz") or has_glob_suffix(repo, "_fuzz.go", "fuzz_target.rs"):
219
+ return True, "fuzz dir/targets"
220
+ if any(x in deps for x in ("@jazzer.js/core", "jazzer")) or "atheris" in py_dep_text(repo):
221
+ return True, "fuzz tooling dep"
222
+ return False, "no fuzz test infrastructure detected"
223
+
224
+
225
+ def d_sanitizers(repo, deps):
226
+ for f in ("Makefile", "CMakeLists.txt"):
227
+ p = os.path.join(repo, f)
228
+ if os.path.isfile(p):
229
+ try:
230
+ if "-fsanitize" in open(p, "r", encoding="utf-8").read():
231
+ return True, "-fsanitize in build config"
232
+ except Exception:
233
+ pass
234
+ return False, "no sanitizer configuration detected"
235
+
236
+
237
+ DETECTORS = {
238
+ "unit": d_unit, "integration": d_integration, "e2e": d_e2e, "smoke": d_smoke,
239
+ "perf": d_perf, "a11y": d_a11y, "contract": d_contract, "migration": d_migration,
240
+ "property-based": d_property, "fuzz": d_fuzz, "sanitizers": d_sanitizers,
241
+ }
242
+
243
+
244
+ # --------------------------------------------------------------------------- #
245
+ def make_row(gate_id, result, *, policy_hash, input_hash, commit_sha, runner,
246
+ metadata=None, failure_mode=None, advisory_severity=None):
247
+ row = {
248
+ "gate_id": gate_id, "result": result, "policy_hash": policy_hash,
249
+ "input_hash": input_hash,
250
+ "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
251
+ "runner": runner, "commit_sha": commit_sha,
252
+ }
253
+ if metadata:
254
+ row["metadata"] = metadata
255
+ if failure_mode is not None:
256
+ row["failure_mode"] = failure_mode
257
+ if advisory_severity is not None:
258
+ row["advisory_severity"] = advisory_severity
259
+ return row
260
+
261
+
262
+ def gate_suffix(gate_id):
263
+ return gate_id.rsplit(":", 1)[-1]
264
+
265
+
266
+ def run_crap(repo, gate, commit_sha, runner, strict):
267
+ enforcement = gate.get("enforcement", "advisory")
268
+ try:
269
+ proc = subprocess.run([sys.executable, os.path.join(HERE, "crap-score.py")],
270
+ cwd=repo, capture_output=True, text=True, timeout=120)
271
+ ok = proc.returncode == 0
272
+ detail = (proc.stdout or proc.stderr).strip().splitlines()[-1:] if (proc.stdout or proc.stderr) else []
273
+ except Exception as e:
274
+ return make_row(gate["gate_id"], "ADVISORY", policy_hash=sha256_str("crap:default"),
275
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
276
+ advisory_severity="warn",
277
+ metadata={"method": "crap-static", "indeterminate": True, "reason": str(e)})
278
+ if ok:
279
+ return make_row(gate["gate_id"], "PASS", policy_hash=sha256_str("crap:default"),
280
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
281
+ metadata={"method": "crap-static", "detail": detail})
282
+ result, fm, sev = ("FAIL", "testing-depth:crap-threshold", None) if (strict or enforcement == "blocking") \
283
+ else ("ADVISORY", None, "error")
284
+ return make_row(gate["gate_id"], result, policy_hash=sha256_str("crap:default"),
285
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
286
+ failure_mode=fm, advisory_severity=sev,
287
+ metadata={"method": "crap-static", "detail": detail})
288
+
289
+
290
+ def run_presence(suffix, repo, deps, gate, commit_sha, runner, strict):
291
+ enforcement = gate.get("enforcement", "advisory")
292
+ present, signal = DETECTORS[suffix](repo, deps)
293
+ if present is True:
294
+ return make_row(gate["gate_id"], "PASS", policy_hash=sha256_str(f"presence:{suffix}"),
295
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
296
+ metadata={"method": "presence-heuristic", "layer": suffix, "signal": signal})
297
+ if present is None:
298
+ return make_row(gate["gate_id"], "ADVISORY", policy_hash=sha256_str(f"presence:{suffix}"),
299
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
300
+ advisory_severity="warn",
301
+ metadata={"method": "presence-heuristic", "layer": suffix,
302
+ "indeterminate": True, "reason": signal})
303
+ # gap
304
+ result, fm, sev = ("FAIL", f"testing-depth:{suffix}-gap", None) if (strict or enforcement == "blocking") \
305
+ else ("ADVISORY", None, "warn")
306
+ return make_row(gate["gate_id"], result, policy_hash=sha256_str(f"presence:{suffix}"),
307
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
308
+ failure_mode=fm, advisory_severity=sev,
309
+ metadata={"method": "presence-heuristic", "layer": suffix, "reason": signal})
310
+
311
+
312
+ def compute_profile(repo, registry_path, profile_arg):
313
+ if profile_arg == "-":
314
+ return json.load(sys.stdin)
315
+ if profile_arg:
316
+ with open(profile_arg, "r", encoding="utf-8") as f:
317
+ return json.load(f)
318
+ out = subprocess.run([sys.executable, os.path.join(HERE, "classify.py"), repo,
319
+ "--registry", registry_path], capture_output=True, text=True)
320
+ if out.returncode != 0:
321
+ sys.stderr.write(out.stderr)
322
+ raise SystemExit(2)
323
+ return json.loads(out.stdout)
324
+
325
+
326
+ def main():
327
+ ap = argparse.ArgumentParser(description="Read-only testing-depth gate-runner -> gate-result/v1 rows")
328
+ ap.add_argument("repo", nargs="?", default=".")
329
+ ap.add_argument("--fast", action="store_true", help="presence heuristics only (default tier)")
330
+ ap.add_argument("--deep", action="store_true", help="presence + crap-score")
331
+ ap.add_argument("--strict", action="store_true", help="treat a testing-depth gap as FAIL (exit 1)")
332
+ ap.add_argument("--registry", default=C.DEFAULT_REGISTRY)
333
+ ap.add_argument("--profile", default=None, help="pinned audit-profile/v1 (PATH or '-')")
334
+ args = ap.parse_args()
335
+
336
+ deep = args.deep and not args.fast
337
+ repo = os.path.abspath(args.repo)
338
+ runner = f"audit-harness@{C.harness_version()}"
339
+
340
+ override_path = os.path.join(repo, ".audit-harness.yml")
341
+ override = C.parse_override(override_path) if os.path.isfile(override_path) else {"disable": False}
342
+ if override.get("disable") or os.environ.get("AUDIT_HARNESS_DISABLE") == "1":
343
+ sys.stderr.write("audit-harness: KILL-SWITCH active — audit skipped (no rows emitted)\n")
344
+ print("[]")
345
+ sys.exit(0)
346
+
347
+ profile = compute_profile(repo, os.path.abspath(args.registry), args.profile)
348
+ commit_sha = profile.get("subject", {}).get("commit_sha") or C.git_short_sha(repo)
349
+ deps = collect_node_deps(repo)
350
+
351
+ gates = [g for g in profile.get("gates", [])
352
+ if g.get("dimension") == "testing-depth" and g.get("enforcement") != "disabled"]
353
+
354
+ rows = []
355
+ for gate in gates:
356
+ suffix = gate_suffix(gate["gate_id"])
357
+ if suffix == "crap-score":
358
+ if not deep:
359
+ rows.append(make_row(gate["gate_id"], "ADVISORY", policy_hash=sha256_str("crap:default"),
360
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
361
+ advisory_severity="info",
362
+ metadata={"method": "crap-static", "skipped": "deep-only (run with --deep)"}))
363
+ else:
364
+ rows.append(run_crap(repo, gate, commit_sha, runner, args.strict))
365
+ elif suffix in DETECTORS:
366
+ rows.append(run_presence(suffix, repo, deps, gate, commit_sha, runner, args.strict))
367
+ else:
368
+ # e.g. per-package-classify — assessment delegated, not a static signal
369
+ rows.append(make_row(gate["gate_id"], "ADVISORY", policy_hash=sha256_str(f"audit:{suffix}"),
370
+ input_hash=EMPTY_SHA, commit_sha=commit_sha, runner=runner,
371
+ advisory_severity="info",
372
+ metadata={"method": "delegated", "indeterminate": True,
373
+ "reason": f"'{suffix}' has no static testing-depth heuristic "
374
+ f"in this harness version"}))
375
+
376
+ print(json.dumps(rows, indent=2))
377
+ n_fail = sum(1 for r in rows if r["result"] == "FAIL")
378
+ n_gap = sum(1 for r in rows if r["result"] == "ADVISORY" and r.get("advisory_severity") == "warn")
379
+ n_pass = sum(1 for r in rows if r["result"] == "PASS")
380
+ sys.stderr.write(f"audit-harness audit ({'deep' if deep else 'fast'}): {n_pass} PASS, "
381
+ f"{n_gap} gap(s), {n_fail} FAIL across {len(rows)} testing-depth gate(s)\n")
382
+ sys.exit(1 if n_fail else 0)
383
+
384
+
385
+ if __name__ == "__main__":
386
+ main()