4"""Inventory every suppression and reconcile it against the review ledger.
6Every recognized waiver row carries a durable two-part identity: a
7``site_id`` naming the occurrence independent of line movement and a
8``binding_sha256`` over exactly what a reviewer approved. The committed
9ledger (``.github/suppression-review-ledger.tsv`` with its rationale and
10batch authorities) binds each site to a reviewed decision; only a ``retain``
11row with an exact binding match marks a suppression approved, and nothing in
12this tool generates approval. ``--check`` stays nonzero while any site is
13unreviewed, carries an unremediated fix decision, or any integrity finding
14fires; exit 2 means the scan itself stopped being trustworthy. An exit-zero
15``--inventory`` means only that report generation succeeded.
19 python3 scripts/checks/check_suppressions.py --selftest
20 python3 scripts/checks/check_suppressions.py --inventory --format json
21 python3 scripts/checks/check_suppressions.py --inventory --format markdown
22 python3 scripts/checks/check_suppressions.py --check
23 python3 scripts/checks/check_suppressions.py --ledger-candidates
24 python3 scripts/checks/check_suppressions.py --list-files
27from __future__
import annotations
32from pathlib
import Path
34sys.path.insert(0, str(Path(__file__).resolve().parent))
36from suppression_ledger
import candidate_rows
37from suppression_model
import Inventory, Suppression
38from suppression_scan
import MIN_REPOSITORY_FILES, git_paths, scan_repository
39from suppression_selftest
import run_selftest
41REPO_ROOT = Path(__file__).resolve().parents[2]
43INTEGRITY_CODES = frozenset(
46 "invalid-text-encoding",
47 "duplicate-fingerprint",
48 "duplicate-site-identity",
49 "baseline-owner-mismatch",
50 "baseline-ceiling-integrity",
52 "duplicate-baseline-row",
53 "duplicate-coverage-mask",
54 "duplicate-mcdc-deactivation",
55 "malformed-baseline-row",
56 "malformed-coverage-mask",
57 "malformed-mcdc-deactivation",
58 "malformed-mcdc-macro",
59 "malformed-native-test-skip",
60 "missing-baseline-file",
61 "missing-baseline-ceilings",
62 "missing-baseline-consumer",
63 "missing-baseline-provenance",
64 "missing-baseline-total",
65 "mcdc-owner-mismatch",
66 "stale-baseline-path",
67 "stranded-branch-marker",
68 "stranded-line-marker",
69 "stale-baseline-total",
70 "unknown-baseline-file",
72 "unexpected-family-count",
73 "unpaired-mcdc-deactivation",
77 "malformed-ruff-config",
78 "malformed-tool-config",
79 "malformed-ansible-lint-config",
80 "malformed-ci-parity-workflow",
81 "malformed-doxygen-config",
82 "malformed-generated-marker",
83 "generated-marker-without-body",
84 "non-substantive-waiver-reason",
85 "self-generated-provenance",
86 "symlinked-generated-provenance",
87 "missing-generated-provenance",
88 "malformed-gitignore-scope-marker",
89 "malformed-global-exclusion-config",
90 "duplicate-file-size-waiver",
91 "untracked-generated-provenance",
92 "unterminated-cmake-bracket",
94 "checker-scope-authority-count",
95 "checker-scope-value-count",
96 "checker-scope-value-digest",
97 "checker-scope-reason-digest",
98 "checker-classification-digest",
99 "checker-census-floor",
100 "checker-authority-mutation",
101 "checker-authority-rebinding",
102 "condition-dependent-authority",
103 "inline-scope-literal",
104 "non-authority-shape-mismatch",
105 "stale-checker-classification",
106 "unclassified-checker-constant",
107 "missing-checker-scope-authority",
108 "unresolved-checker-scope-authority",
109 "checker-nonfatal-ast",
110 "checker-nonfatal-declaration-count",
111 "missing-nonfatal-authority",
112 "missing-nonfatal-declaration",
113 "active-nonfatal-constant",
114 "active-checker-nonfatal-invocation",
115 "nonfatal-informational-count",
116 "unexpected-governance-count",
119 "unterminated-comment",
120 "unterminated-html-comment",
121 "unterminated-heredoc",
122 "unterminated-line-comment-splice",
123 "unterminated-shell-quote",
124 "vacuous-baseline-files",
125 "vacuous-baseline-rows",
126 "unterminated-shell-arithmetic",
127 "unterminated-string",
131 "missing-review-ledger",
132 "malformed-review-ledger",
133 "ledger-duplicate-batch",
134 "ledger-duplicate-site",
135 "ledger-schema-mismatch",
136 "ledger-unknown-reference",
137 "ledger-batch-mismatch",
138 "ledger-state-conflict",
139 "ledger-binding-mismatch",
141 "ledger-resolved-still-present",
146def _parser() -> argparse.ArgumentParser:
147 """Build the explicit-mode command-line parser."""
148 parser = argparse.ArgumentParser(description=__doc__)
149 modes = parser.add_mutually_exclusive_group(required=
True)
150 modes.add_argument(
"--selftest", action=
"store_true", help=
"run both-direction fixtures")
154 help=
"emit the ledger-reconciled inventory",
156 modes.add_argument(
"--check", action=
"store_true", help=
"fail on concerns or findings")
158 "--ledger-candidates",
160 help=
"print unreviewed ledger candidate rows for every live site",
165 help=
"list JSON-escaped scan paths (use -z for exact NUL-delimited paths)",
167 parser.add_argument(
"--format", choices=(
"json",
"markdown"), help=
"inventory output format")
168 parser.add_argument(
"-z",
"--null", action=
"store_true", help=
"NUL-terminate --list-files")
172def _markdown_escape(value: object) -> str:
173 """Escape one scalar for a Markdown table cell."""
174 return str(value).replace(
"|",
"\\|").replace(
"\n",
" ")
177def _render_summary(inventory: Inventory) -> list[str]:
178 """Render deterministic Markdown summary bullets."""
179 families = inventory.family_counts()
181 f
"- Files scanned: {inventory.files_scanned}",
182 f
"- Text files: {inventory.text_files}",
183 f
"- Binary files: {inventory.binary_files}",
184 f
"- Suppressions: {len(inventory.suppressions)}",
185 f
"- Scanner findings: {len(inventory.findings)}",
189 "- Families: " +
", ".join(f
"{key}={value}" for key, value
in families.items())
194def _render_markdown(inventory: Inventory) -> str:
195 """Render the non-authoritative phase-one Markdown inventory."""
197 "# Suppression inventory",
199 *_render_summary(inventory),
204 "| Path | Line | Family | Tool | Rule | Scope | Owner | Reason | Concerns |",
205 "|---|---:|---|---|---|---|---|---|---|",
208 ordered = sorted(inventory.suppressions, key=
lambda item: (item.path, item.line, item.column))
219 ", ".join(item.concerns),
221 lines.append(
"| " +
" | ".join(_markdown_escape(value)
for value
in values) +
" |")
222 lines.extend([
"",
"## Scanner findings",
""])
223 if not inventory.findings:
224 lines.append(
"None.")
226 for finding
in sorted(
227 inventory.findings, key=
lambda item: (item.path, item.line, item.code)
229 location = f
"{finding.path}:{finding.line}" if finding.path
else "repository"
230 lines.append(f
"- `{finding.code}` at `{location}`: {_markdown_escape(finding.message)}")
231 return "\n".join(lines) +
"\n"
234def _integrity_failed(inventory: Inventory) -> bool:
235 """Return whether scanner evidence is malformed or vacuous."""
236 return any(finding.code
in INTEGRITY_CODES
for finding
in inventory.findings)
239def _review_count(inventory: Inventory) -> int:
240 """Count scanner findings and per-row review concerns.
242 Concerns on an approved row do not count again: the ledger binding the
243 approval hashes the concerns, so any change reopens the review instead.
245 pending = sum(item.disposition !=
"approved" for item
in inventory.suppressions)
247 len(inventory.findings)
249 len(item.concerns)
for item
in inventory.suppressions
if item.disposition !=
"approved"
255def _concern_line(item: Suppression, concern: str) -> str:
256 """Format one suppression concern for check-mode diagnostics."""
257 return f
"{item.path}:{item.line}: {concern}: {item.family}/{item.rule}"
260def _run_check(inventory: Inventory) -> int:
261 """Print bounded diagnostics and return clean, debt, or malformed status."""
263 f
"{item.path or 'repository'}:{item.line}: {item.code}: {item.message}"
264 for item
in inventory.findings
267 _concern_line(item, concern)
268 for item
in inventory.suppressions
269 if item.disposition !=
"approved"
270 for concern
in item.concerns
273 f
"{item.path}:{item.line}: governance-pending: {item.fingerprint}"
274 for item
in inventory.suppressions
275 if item.disposition !=
"approved"
277 for detail
in details[:MAX_CHECK_DETAILS]:
279 if len(details) > MAX_CHECK_DETAILS:
280 print(f
"... {len(details) - MAX_CHECK_DETAILS} additional item(s) omitted")
282 f
"check_suppressions.py: {len(inventory.suppressions)} suppression(s), "
283 f
"{_review_count(inventory)} review item(s)"
285 if _integrity_failed(inventory):
287 return 1
if details
else 0
290def _run_list_files(*, null: bool) -> int:
291 """List exact scan candidates and reject a collapsed Git enumeration."""
292 paths, findings = git_paths(REPO_ROOT)
294 for finding
in findings:
295 print(f
"check_suppressions.py: FATAL -- {finding.message}", file=sys.stderr)
297 if len(paths) < MIN_REPOSITORY_FILES:
299 f
"check_suppressions.py: FATAL -- only {len(paths)} file(s); "
300 f
"floor is {MIN_REPOSITORY_FILES}",
305 sys.stdout.buffer.write(b
"\0".join(path.encode(
"utf-8")
for path
in paths) + b
"\0")
307 print(
"\n".join(json.dumps(path, ensure_ascii=
True)
for path
in paths))
311def main(argv: list[str] |
None =
None) -> int:
312 """Run the selected public scanner mode."""
314 args = parser.parse_args(argv)
315 if args.format
is not None and not args.inventory:
316 parser.error(
"--format requires --inventory")
317 if args.null
and not args.list_files:
318 parser.error(
"--null requires --list-files")
320 return run_selftest()
322 return _run_list_files(null=args.null)
323 inventory, _ = scan_repository(REPO_ROOT)
324 if args.ledger_candidates:
325 print(
"site_id\tbinding_sha256\tstate\trationale_id\tbatch_id\tevidence_ref")
326 for row
in candidate_rows(inventory):
328 return 2
if _integrity_failed(inventory)
else 0
330 if (args.format
or "json") ==
"json":
331 print(json.dumps(inventory.as_dict(), indent=2, sort_keys=
True, ensure_ascii=
True))
333 print(_render_markdown(inventory), end=
"")
334 return 2
if _integrity_failed(inventory)
else 0
335 return _run_check(inventory)
338if __name__ ==
"__main__":
339 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.