Spaces:
Sleeping
Sleeping
Commit Β·
5876e7b
1
Parent(s): a519e2c
feat(audit): Tier 1 repo-integrity checks + selftest fixtures
Browse filesCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- audit/selftest_fixtures.py +96 -0
- audit/tier1_repo.py +83 -1
audit/selftest_fixtures.py
CHANGED
|
@@ -1 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
FIXTURES: dict = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Selftest fixtures: each yields a context where the matching check FAILs.
|
| 2 |
+
|
| 3 |
+
Every fixture creates an obviously-temporary broken state under REPO, yields,
|
| 4 |
+
then fully restores in a `finally` so the repo is not left dirty.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
import contextlib
|
| 8 |
+
import os
|
| 9 |
+
from audit.core import REPO, sh
|
| 10 |
+
|
| 11 |
FIXTURES: dict = {}
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@contextlib.contextmanager
|
| 15 |
+
def _f_t1_1():
|
| 16 |
+
"""Track a symlink (mode 120000) so T1.1 FAILs."""
|
| 17 |
+
link = REPO / "_audit_selftest_symlink"
|
| 18 |
+
if link.exists() or link.is_symlink():
|
| 19 |
+
link.unlink()
|
| 20 |
+
os.symlink("README.md", link)
|
| 21 |
+
sh(["git", "add", "-f", "_audit_selftest_symlink"])
|
| 22 |
+
try:
|
| 23 |
+
yield
|
| 24 |
+
finally:
|
| 25 |
+
sh(["git", "rm", "--cached", "-q", "_audit_selftest_symlink"])
|
| 26 |
+
if link.exists() or link.is_symlink():
|
| 27 |
+
link.unlink()
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@contextlib.contextmanager
|
| 31 |
+
def _f_t1_2():
|
| 32 |
+
"""Track a >512KB file with an extension not covered by any LFS glob."""
|
| 33 |
+
big = REPO / "_audit_selftest_big.dat"
|
| 34 |
+
big.write_bytes(b"A" * (768 * 1024))
|
| 35 |
+
sh(["git", "add", "-f", "_audit_selftest_big.dat"])
|
| 36 |
+
try:
|
| 37 |
+
yield
|
| 38 |
+
finally:
|
| 39 |
+
sh(["git", "rm", "--cached", "-q", "_audit_selftest_big.dat"])
|
| 40 |
+
if big.exists():
|
| 41 |
+
big.unlink()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@contextlib.contextmanager
|
| 45 |
+
def _f_t1_3():
|
| 46 |
+
"""Track a file containing a key-shaped string so T1.3 FAILs."""
|
| 47 |
+
secret = REPO / "_audit_selftest_secret.txt"
|
| 48 |
+
secret.write_text("token = hf_" + "a1B2c3D4e5F6g7H8i9J0kLmNoP\n", encoding="utf-8")
|
| 49 |
+
sh(["git", "add", "-f", "_audit_selftest_secret.txt"])
|
| 50 |
+
try:
|
| 51 |
+
yield
|
| 52 |
+
finally:
|
| 53 |
+
sh(["git", "rm", "--cached", "-q", "_audit_selftest_secret.txt"])
|
| 54 |
+
if secret.exists():
|
| 55 |
+
secret.unlink()
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@contextlib.contextmanager
|
| 59 |
+
def _f_t1_4():
|
| 60 |
+
"""Remove the slash-less `rag/corpus` line from .gitignore so T1.4 FAILs."""
|
| 61 |
+
gi = REPO / ".gitignore"
|
| 62 |
+
original = gi.read_text(encoding="utf-8")
|
| 63 |
+
patched = "\n".join(
|
| 64 |
+
ln for ln in original.split("\n") if ln != "rag/corpus"
|
| 65 |
+
)
|
| 66 |
+
gi.write_text(patched, encoding="utf-8")
|
| 67 |
+
try:
|
| 68 |
+
yield
|
| 69 |
+
finally:
|
| 70 |
+
gi.write_text(original, encoding="utf-8")
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@contextlib.contextmanager
|
| 74 |
+
def _f_t1_5():
|
| 75 |
+
"""Track a path containing .DS_Store so T1.5 FAILs."""
|
| 76 |
+
d = REPO / "_audit_selftest_dir"
|
| 77 |
+
d.mkdir(exist_ok=True)
|
| 78 |
+
junk = d / ".DS_Store"
|
| 79 |
+
junk.write_bytes(b"\x00junk\x00")
|
| 80 |
+
sh(["git", "add", "-f", "_audit_selftest_dir/.DS_Store"])
|
| 81 |
+
try:
|
| 82 |
+
yield
|
| 83 |
+
finally:
|
| 84 |
+
sh(["git", "rm", "--cached", "-q", "_audit_selftest_dir/.DS_Store"])
|
| 85 |
+
if junk.exists():
|
| 86 |
+
junk.unlink()
|
| 87 |
+
if d.exists():
|
| 88 |
+
d.rmdir()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
FIXTURES.update({
|
| 92 |
+
"T1.1": _f_t1_1,
|
| 93 |
+
"T1.2": _f_t1_2,
|
| 94 |
+
"T1.3": _f_t1_3,
|
| 95 |
+
"T1.4": _f_t1_4,
|
| 96 |
+
"T1.5": _f_t1_5,
|
| 97 |
+
})
|
audit/tier1_repo.py
CHANGED
|
@@ -1 +1,83 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tier 1 β repo integrity (pre-commit, fast)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import re
|
| 4 |
+
from audit.core import register, Result, Status, REPO, git, sh
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@register("T1.1", "static", "no tracked symlinks")
|
| 8 |
+
def t1_1() -> Result:
|
| 9 |
+
out = git("ls-files", "-s")
|
| 10 |
+
syms = [ln.split("\t", 1)[1] for ln in out.splitlines() if ln.startswith("120000")]
|
| 11 |
+
if syms:
|
| 12 |
+
return Result("T1.1", Status.FAIL, f"tracked symlinks: {syms}",
|
| 13 |
+
"git rm --cached <path>; add to .gitignore (no trailing slash)")
|
| 14 |
+
return Result("T1.1", Status.PASS, "no tracked symlinks")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@register("T1.2", "static", "LFS coverage for binary/large files")
|
| 18 |
+
def t1_2() -> Result:
|
| 19 |
+
ga = (REPO / ".gitattributes").read_text(encoding="utf-8", errors="replace")
|
| 20 |
+
lfs_globs = [ln.split()[0] for ln in ga.splitlines()
|
| 21 |
+
if "filter=lfs" in ln and ln.strip() and not ln.startswith("#")]
|
| 22 |
+
lfs_files = set(sh(["git", "lfs", "ls-files", "-n"]).stdout.split())
|
| 23 |
+
bad = []
|
| 24 |
+
for ln in git("ls-files", "-s").splitlines():
|
| 25 |
+
parts = ln.split()
|
| 26 |
+
mode, blob, path = parts[0], parts[1], ln.split("\t", 1)[1] if "\t" in ln else parts[3]
|
| 27 |
+
if mode == "120000":
|
| 28 |
+
continue
|
| 29 |
+
szout = sh(["git", "cat-file", "-s", blob]).stdout.strip()
|
| 30 |
+
is_lfs = path in lfs_files
|
| 31 |
+
big = szout.isdigit() and int(szout) > 512 * 1024
|
| 32 |
+
if big and not is_lfs:
|
| 33 |
+
bad.append(f"{path} ({int(szout)//1024} KB) not LFS")
|
| 34 |
+
if bad:
|
| 35 |
+
return Result("T1.2", Status.FAIL, "; ".join(bad[:8]),
|
| 36 |
+
"add a filter=lfs rule to .gitattributes; git rm --cached + re-add the files")
|
| 37 |
+
return Result("T1.2", Status.PASS, f"{len(lfs_files)} LFS files; no oversized non-LFS blobs")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@register("T1.3", "static", "no real secrets tracked")
|
| 41 |
+
def t1_3() -> Result:
|
| 42 |
+
KEYISH = re.compile(r"(hf_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{20,}|AIza[A-Za-z0-9_\-]{20,}|xox[bp]-[A-Za-z0-9-]{20,})")
|
| 43 |
+
suspects = []
|
| 44 |
+
for path in git("ls-files").splitlines():
|
| 45 |
+
base = path.rsplit("/", 1)[-1]
|
| 46 |
+
if base == ".env" or (base.startswith(".env") and not base.endswith((".example", ".sample"))):
|
| 47 |
+
suspects.append(f"{path} (real dotenv tracked)")
|
| 48 |
+
continue
|
| 49 |
+
if path.endswith((".png", ".jpg", ".jpeg", ".pdf", ".duckdb", ".bin", ".ico", ".woff", ".woff2", ".ttf")):
|
| 50 |
+
continue
|
| 51 |
+
try:
|
| 52 |
+
txt = (REPO / path).read_text(encoding="utf-8", errors="ignore")
|
| 53 |
+
except Exception:
|
| 54 |
+
continue
|
| 55 |
+
if KEYISH.search(txt) and "example" not in path and "ADR-010" not in path:
|
| 56 |
+
suspects.append(f"{path} (key-shaped string)")
|
| 57 |
+
if suspects:
|
| 58 |
+
return Result("T1.3", Status.FAIL, "; ".join(suspects[:8]),
|
| 59 |
+
"remove from index + history; rotate the key; gitignore the file")
|
| 60 |
+
return Result("T1.3", Status.PASS, "no real .env / key material tracked")
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@register("T1.4", "static", ".gitignore robust for file AND dir")
|
| 64 |
+
def t1_4() -> Result:
|
| 65 |
+
intents = ["tools/.pdf_text_cache", "rag/corpus", "rag/extracted", "rag/vectors"]
|
| 66 |
+
gi = (REPO / ".gitignore").read_text(encoding="utf-8", errors="replace").splitlines()
|
| 67 |
+
missing = [it for it in intents if it not in gi]
|
| 68 |
+
if missing:
|
| 69 |
+
return Result("T1.4", Status.FAIL, f"only dir-form (or absent) ignore for: {missing}",
|
| 70 |
+
"add a slash-less line per intent so a symlink/file of that name is also ignored")
|
| 71 |
+
return Result("T1.4", Status.PASS, "file+dir ignore intents present")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
@register("T1.5", "static", "no junk/build artifacts tracked")
|
| 75 |
+
def t1_5() -> Result:
|
| 76 |
+
JUNK = ("tools/.pdf_text_cache/", ".pytest_cache/", ".DS_Store",
|
| 77 |
+
"frontend/out/", "frontend/.next/", "node_modules/", ".tsbuildinfo")
|
| 78 |
+
tracked = git("ls-files").splitlines()
|
| 79 |
+
hits = [p for p in tracked if any(j in p for j in JUNK)]
|
| 80 |
+
if hits:
|
| 81 |
+
return Result("T1.5", Status.FAIL, f"{len(hits)} junk paths e.g. {hits[:5]}",
|
| 82 |
+
"git rm -r --cached <path> and gitignore it")
|
| 83 |
+
return Result("T1.5", Status.PASS, "no caches/build artifacts tracked")
|