3"""Syntax-aware scanners for suppression and waiver directives."""
5from __future__
import annotations
11from collections
import Counter
12from dataclasses
import dataclass, field
13from pathlib
import Path
15from suppression_baseline_scan
import scan_baseline_repository
16from suppression_build_controls
import compiler_records
17from suppression_c_control_scan
import C_CONTROL_HINT_RE, scan_c_controls
18from suppression_c_controls
import scan_c_control_file
19from suppression_catalog
import (
24 UNSUPPORTED_CATEGORIES,
28from suppression_checker_nonfatal
import scan_checker_nonfatal_controls
29from suppression_checker_scope
import EXPECTED_VALUES, scan_checker_scope_controls
30from suppression_clang_tidy
import scan_clang_tidy_config
31from suppression_comment_lex
import extract_comments
32from suppression_control_scan
import scan_control_file
33from suppression_coverage_scan
import scan_coverage_masks
34from suppression_generated_markers
import generated_records
35from suppression_governance
import scan_ci_parity_exemptions, scan_governance_file
36from suppression_hardware_todo
import scan_hardware_todo_controls
37from suppression_identity
import assign_identities
38from suppression_inline_scan
import (
43 stranded_branch_findings,
45from suppression_ledger
import apply_ledger
46from suppression_model
import Finding, Inventory, Suppression
47from suppression_shell_scan
import shell_status_records, yaml_shell_block_status_records
48from suppression_tool_controls
import (
49 configured_optional_tools,
53from suppression_validate
import (
55 validate_cppcheck_anchors,
56 validate_fingerprints,
60MIN_REPOSITORY_FILES = 2000
64 "mcdc-deactivation": 77,
66EXPECTED_EXACT_FAMILY_COUNTS = {
67 "ansible-lint-config": 0,
68 "other-language-control": 0,
69 "security-analysis-control": 0,
72GOVERNANCE_COUNT_CONTRACTS = {
73 "ansible-lint-config": (
"family",
"ansible-lint-config", 0),
74 "ci-parity-exemptions": (
"family",
"ci-parity-exemption", 3),
75 "generated-artifacts": (
"family",
"generated-artifact", 10),
76 "doxygen-controls": (
"family",
"documentation-control", 17),
77 "security-analysis-controls": (
"family",
"security-analysis-control", 0),
78 "other-language-controls": (
"family",
"other-language-control", 0),
79 "gitignore-scope-exemptions": (
"tool",
"gitignore-scope", 1),
80 "hardware-canned-stub-waivers": (
"tool",
"check-no-silent-stubs", 0),
81 "checker-scope-values": (
"family",
"checker-scope-control", EXPECTED_VALUES),
82 "checker-nonfatal-declarations": (
"directive",
"nonfatal-declaration", 10),
83 "checker-nonfatal-invocations": (
"directive",
"nonfatal-invocation", 0),
84 "vendor-encoding-exemptions": (
"family",
"encoding-exemption", 4),
89class CppcheckReasonState:
90 """Rationale association state for the central cppcheck suppression list."""
92 in_section: bool =
False
93 section_notes: list[str] = field(default_factory=list)
94 local_notes: list[str] = field(default_factory=list)
95 frozen_reason: str =
""
96 last_was_entry: bool =
False
98 def add_comment(self, note: str) ->
None:
99 """Consume one comment or paired section delimiter."""
100 if note
and set(note) <= {
"-"}:
102 self.frozen_reason =
" ".join(self.section_notes).strip()
103 self.in_section =
False
105 self.in_section =
True
106 self.section_notes = []
107 self.frozen_reason =
""
108 self.local_notes = []
109 self.last_was_entry =
False
110 elif self.in_section:
111 self.section_notes.append(note)
113 if self.last_was_entry:
114 self.local_notes = []
115 self.frozen_reason =
""
116 self.local_notes.append(note)
117 self.last_was_entry =
False
119 def reason_for_entry(self) -> str:
120 """Freeze an open section and return the rationale for the next entry."""
122 self.frozen_reason =
" ".join(self.section_notes).strip()
123 self.in_section =
False
124 self.last_was_entry =
True
125 return self.frozen_reason
or " ".join(self.local_notes).strip()
128def git_paths(root: Path) -> tuple[list[str], list[Finding]]:
129 """Enumerate present tracked and nonignored untracked repository files."""
130 proc = subprocess.run(
131 [
"git",
"ls-files",
"-z",
"--cached",
"--others",
"--exclude-standard"],
136 if proc.returncode != 0:
137 message = proc.stderr.decode(
"utf-8", errors=
"replace").strip()
138 return [], [Finding(
"git-enumeration", message
or "git ls-files failed")]
140 message = proc.stderr.decode(
"utf-8", errors=
"replace").strip()
141 return [], [Finding(
"git-enumeration", message
or "git ls-files emitted diagnostics")]
142 return decode_git_paths(proc.stdout, root)
145def decode_git_paths(data: bytes, _root: Path) -> tuple[list[str], list[Finding]]:
146 """Decode Git's NUL list and reject names that are not valid UTF-8."""
148 decoded = data.decode(
"utf-8", errors=
"strict").split(
"\0")
149 except UnicodeDecodeError
as exc:
150 return [], [Finding(
"git-enumeration", f
"non-UTF-8 repository path: {exc}")]
151 paths = [path
for path
in decoded
if path]
152 return sorted(paths), []
155def _read_text(root: Path, rel: str) -> tuple[str |
None, Finding |
None]:
156 """Read one candidate as UTF-8 text, classifying binary data explicitly."""
159 resolved = path.resolve(strict=
True)
160 resolved.relative_to(root)
161 except (OSError, ValueError)
as exc:
162 if isinstance(exc, ValueError):
163 failure = Finding(
"unsafe-symlink",
"path resolves outside repository")
165 failure = Finding(
"read-error", str(exc))
167 if path.suffix.lower()
in BINARY_SUFFIXES:
170 with resolved.open(
"rb")
as handle:
171 prefix = handle.read(8192)
174 data = prefix + handle.read()
175 except OSError
as exc:
176 return None, Finding(
"read-error", str(exc))
178 return data.decode(
"utf-8"),
None
179 except UnicodeDecodeError
as exc:
180 return None, Finding(
"invalid-text-encoding", str(exc))
183def _ruff_config_record(path: str, line: int, recognition: Recognition) -> Suppression:
184 """Build one source-located Ruff central-configuration inventory row."""
192 recognition.directive,
197 _concerns(recognition.rule, recognition.reason),
201def _toml_section_lines(text: str, name: str) -> list[tuple[int, str]]:
202 """Return source-located lines from one exact TOML table."""
203 result: list[tuple[int, str]] = []
205 for line_no, raw
in enumerate(text.splitlines(), start=1):
206 stripped = raw.strip()
207 if stripped.startswith(
"[")
and stripped.endswith(
"]"):
208 active = stripped == f
"[{name}]"
210 result.append((line_no, raw))
214def _ruff_list_entries(text: str, section: str, key: str) -> list[tuple[int, str, str]]:
215 """Return line, string value, and rationale from one Ruff string array."""
216 result: list[tuple[int, str, str]] = []
218 pattern = re.compile(
r'^\s*"(?P<value>[^"]+)"\s*,?\s*(?:#\s*(?P<reason>.*))?$')
219 for line_no, raw
in _toml_section_lines(text, section):
220 stripped = raw.strip()
221 if stripped == f
"{key} = [":
223 elif active
and stripped ==
"]":
225 elif active
and (match := pattern.fullmatch(raw))
is not None:
226 result.append((line_no, match.group(
"value"), (match.group(
"reason")
or "").strip()))
230def _ruff_per_file_entries(text: str) -> list[tuple[int, str, str, str]]:
231 """Return line, path, rule, and rationale for single-rule Ruff file waivers."""
232 pattern = re.compile(
233 r'^\s*"(?P<path>[^"]+)"\s*=\s*\[\s*"(?P<rule>[^"]+)"\s*\]'
234 r"\s*(?:#\s*(?P<reason>.*))?$"
236 result: list[tuple[int, str, str, str]] = []
237 for line_no, raw
in _toml_section_lines(text,
"tool.ruff.lint.per-file-ignores"):
238 if (match := pattern.fullmatch(raw))
is not None:
244 (match.group(
"reason")
or "").strip(),
250def _ruff_config_records(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
251 """Inventory Ruff global ignores, per-file ignores, and path exclusions."""
252 if path !=
"pyproject.toml":
255 parsed = tomllib.loads(text)
256 ruff = parsed[
"tool"][
"ruff"]
258 except (KeyError, TypeError, tomllib.TOMLDecodeError)
as exc:
259 return [], [Finding(
"malformed-ruff-config", str(exc), path)]
261 excludes = _ruff_list_entries(text,
"tool.ruff",
"extend-exclude")
262 ignores = _ruff_list_entries(text,
"tool.ruff.lint",
"ignore")
263 per_file = _ruff_per_file_entries(text)
268 Recognition(
"python",
"ruff", value,
"ruff extend-exclude",
"path-pattern", reason),
270 for line, value, reason
in excludes
276 Recognition(
"python",
"ruff", value,
"ruff ignore",
"repository", reason),
278 for line, value, reason
in ignores
284 Recognition(
"python",
"ruff", rule,
"ruff per-file-ignore", f
"file:{target}", reason),
286 for line, target, rule, reason
in per_file
289 expected_excludes = ruff.get(
"extend-exclude", [])
290 expected_ignores = lint.get(
"ignore", [])
291 expected_per_file = lint.get(
"per-file-ignores", {})
292 observed_per_file: dict[str, list[str]] = {}
293 for _, target, rule, _
in per_file:
294 observed_per_file.setdefault(target, []).append(rule)
296 [value
for _, value, _
in excludes] != expected_excludes
297 or [value
for _, value, _
in ignores] != expected_ignores
298 or observed_per_file != expected_per_file
300 message =
"source-location parser does not cover every active Ruff waiver"
301 return records, [Finding(
"malformed-ruff-config", message, path)]
305def _unsupported_counts(path: str, text: str, counts: Counter[str]) ->
None:
306 """Count hints for declared phase-one gaps without claiming recognition."""
308 scanner_dir = Path(
"scripts") /
"checks"
309 if item.parent == scanner_dir
and (
310 item.name.startswith(
"suppression_")
or item.name ==
"check_suppressions.py"
313 for name, pattern
in UNSUPPORTED_CATEGORIES:
314 counts[name] += len(pattern.findall(text))
317def _append_unsupported(inventory: Inventory, counts: Counter[str]) ->
None:
318 """Make every known absent recognizer an explicit non-clean check result."""
319 if not UNSUPPORTED_CATEGORIES:
321 for name, _
in UNSUPPORTED_CATEGORIES:
323 message = f
"phase-one recognizer absent; {count} unverified text hint(s)"
324 inventory.findings.append(Finding(
"unsupported-category", f
"{name}: {message}"))
325 message =
"phase-one raw hint census excludes its scanner sources to avoid regex self-hits"
326 inventory.findings.append(Finding(
"scanner-self-exemption", message))
329def _cppcheck_list(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
330 """Parse the central cppcheck suppressions-list format."""
331 records: list[Suppression] = []
332 findings: list[Finding] = []
333 state = CppcheckReasonState()
334 for line_no, raw
in enumerate(text.splitlines(), start=1):
335 stripped = raw.strip()
338 if stripped.startswith(
"#"):
339 note = stripped[1:].strip()
340 state.add_comment(note)
342 parts = stripped.split(
":", 2)
343 rule = parts[0].strip()
344 if rule
not in KNOWN_CPPCHECK_RULES:
345 findings.append(Finding(
"malformed-cppcheck-list", stripped, path, line_no))
347 target = parts[1].strip()
if len(parts) > 1
else "*"
348 scope =
":".join(parts[1:]).strip()
if len(parts) > 1
else "repository"
349 reason = state.reason_for_entry()
350 concerns = _concerns(rule, reason)
367 if len(parts) > 1
and not target:
368 findings.append(Finding(
"malformed-cppcheck-list",
"empty target", path, line_no))
370 findings.append(Finding(
"malformed-cppcheck-list",
"unterminated rationale section", path))
371 return records, findings
374def _nonvacuity(inventory: Inventory) ->
None:
375 """Reject collapsed enumeration or recognizer coverage as malformed."""
376 if inventory.files_scanned < MIN_REPOSITORY_FILES:
377 files = inventory.files_scanned
378 message = f
"only {files} files enumerated; floor is {MIN_REPOSITORY_FILES}"
379 inventory.findings.append(Finding(
"vacuous-files", message))
380 if len(inventory.suppressions) < MIN_SUPPRESSIONS:
381 count = len(inventory.suppressions)
382 message = f
"only {count} suppressions found; floor is {MIN_SUPPRESSIONS}"
383 inventory.findings.append(Finding(
"vacuous-inventory", message))
384 seen = {item.family
for item
in inventory.suppressions}
385 exact_expected = set(EXPECTED_EXACT_FAMILY_COUNTS)
386 for family
in sorted(REQUIRED_FAMILIES - seen - exact_expected):
387 inventory.findings.append(Finding(
"missing-family", family))
388 counts = Counter(item.family
for item
in inventory.suppressions)
389 for family, expected
in EXPECTED_EXACT_FAMILY_COUNTS.items():
390 if counts[family] != expected:
391 message = f
"{family}={counts[family]}; audited exact contract is {expected}"
392 inventory.findings.append(Finding(
"unexpected-family-count", message))
393 for family, floor
in MIN_FAMILY_COUNTS.items():
394 if counts[family] < floor:
395 message = f
"{family}={counts[family]}; audited floor is {floor}"
396 inventory.findings.append(Finding(
"vacuous-family", message))
399def _governance_count_contracts(inventory: Inventory) ->
None:
400 """Lock audited live and zero-live semantic governance populations."""
401 for label, (attribute, value, expected)
in GOVERNANCE_COUNT_CONTRACTS.items():
402 count = sum(getattr(item, attribute) == value
for item
in inventory.suppressions)
403 if count != expected:
404 message = f
"{label}={count}; audited semantic contract is {expected}"
405 inventory.findings.append(Finding(
"unexpected-governance-count", message))
409 inventory: Inventory,
413 active_tools: frozenset[str],
415 """Run comment, compiler, shell, and C-control recognizers after lexing."""
416 comments, findings = extract_comments(rel, text)
417 inventory.findings.extend(Finding(item.code, item.message, rel, item.line)
for item
in findings)
419 records, findings = _scan_raw_nolint(rel, text)
420 inventory.suppressions.extend(records)
421 inventory.findings.extend(findings)
424 comment_lines = frozenset(
427 for line
in range(item.line, item.line + item.text.count(
"\n") + 1)
429 inventory.findings.extend(stranded_branch_findings(rel, text, comment_lines))
430 records, findings = _scan_comments(rel, comments, active_tools, include_nolint=
not c_family)
431 inventory.suppressions.extend(records)
432 inventory.findings.extend(findings)
433 inventory.suppressions.extend(compiler_records(rel, text))
435 records, findings = scan_c_controls(rel, text, comments)
436 inventory.suppressions.extend(records)
437 inventory.findings.extend(findings)
438 records, findings = shell_status_records(rel, text)
439 inventory.suppressions.extend(records)
440 inventory.findings.extend(findings)
441 records, findings = yaml_shell_block_status_records(rel, text)
442 inventory.suppressions.extend(records)
443 inventory.findings.extend(findings)
447 inventory: Inventory,
450 unsupported: Counter[str],
451 active_tools: frozenset[str],
453 """Run every syntax recognizer over one decoded repository file."""
454 inventory.text_files += 1
455 _unsupported_counts(rel, text, unsupported)
456 records, findings = scan_c_control_file(rel, text)
457 inventory.suppressions.extend(records)
458 inventory.findings.extend(findings)
459 first_line = text.partition(
"\n")[0]
460 c_family = language(rel, first_line) ==
"c-family"
463 or SUPPORTED_HINT_RE.search(text)
is not None
464 or text.rstrip(
" \t\n").endswith(
"\\")
465 or C_CONTROL_HINT_RE.search(text)
is not None
468 _scan_lexed_path(inventory, rel, text, c_family, active_tools)
469 if rel ==
".cppcheck-suppressions":
470 records, findings = _cppcheck_list(rel, text)
471 inventory.suppressions.extend(records)
472 inventory.findings.extend(findings)
473 for config_scanner
in (_ruff_config_records, scan_tool_configs, scan_tool_sources):
474 records, findings = config_scanner(rel, text)
475 inventory.suppressions.extend(records)
476 inventory.findings.extend(findings)
477 records, findings = scan_clang_tidy_config(rel, text)
478 inventory.suppressions.extend(records)
479 inventory.findings.extend(findings)
480 records, findings = scan_control_file(rel, text)
481 inventory.suppressions.extend(records)
482 inventory.findings.extend(findings)
484 records, findings = scan_coverage_masks(rel, text)
485 inventory.suppressions.extend(records)
486 inventory.findings.extend(findings)
489def _scan_repository_governance(inventory: Inventory, root: Path, paths: list[str]) ->
None:
490 """Run controls whose truth depends on repository-wide bindings/callers."""
492 scan_ci_parity_exemptions,
493 scan_hardware_todo_controls,
494 scan_checker_scope_controls,
495 scan_checker_nonfatal_controls,
497 records, findings = scanner(root, paths)
498 inventory.suppressions.extend(records)
499 inventory.findings.extend(findings)
502def _vendor_encoding_row(root: Path, rel: str, finding: Finding) -> Suppression | Finding:
503 """Bind one vendored undecodable file to its exact blob hash."""
505 blob = hashlib.sha256((root / rel).read_bytes()).hexdigest()
506 except OSError
as exc:
507 return Finding(
"read-error", str(exc), rel)
512 "encoding-exemption",
513 "suppression-scanner",
514 "invalid-text-encoding",
515 "vendor-encoding-exemption",
516 f
"blob:sha256:{blob}",
517 f
"vendored legacy text is not decoded or scanned: {finding.message}",
524@dataclass(frozen=True)
526 """Per-run scan inputs shared by every candidate path."""
529 unsupported: Counter[str]
530 active_tools: frozenset[str]
531 tracked_paths: frozenset[str]
534def _scan_one_path(inventory: Inventory, context: _ScanContext, rel: str) ->
None:
535 """Read and scan one repository candidate through every recognizer."""
537 text, finding = _read_text(root, rel)
538 if finding
is not None:
539 if finding.code ==
"invalid-text-encoding" and ownership(rel) ==
"vendor":
540 row = _vendor_encoding_row(root, rel, finding)
541 if isinstance(row, Suppression):
542 inventory.suppressions.append(row)
543 inventory.binary_files += 1
545 inventory.findings.append(row)
547 inventory.findings.append(Finding(finding.code, finding.message, rel, finding.line))
550 inventory.binary_files += 1
552 _scan_text_path(inventory, rel, text, context.unsupported, context.active_tools)
553 records, findings = scan_governance_file(rel, text)
554 inventory.suppressions.extend(records)
555 inventory.findings.extend(findings)
556 records, findings = generated_records(rel, text, context.tracked_paths, root)
557 inventory.suppressions.extend(records)
558 inventory.findings.extend(findings)
561def scan_paths(root: Path, paths: list[str], *, enforce_floors: bool =
False) -> Inventory:
562 """Scan explicit repo-relative paths through the production recognizers."""
563 inventory = Inventory(files_scanned=len(paths))
565 resolved_root = root.resolve(strict=
True)
566 except OSError
as exc:
567 inventory.findings.append(Finding(
"read-error", str(exc)))
569 unsupported: Counter[str] = Counter()
570 active_tools = configured_optional_tools(paths)
571 tracked_paths = frozenset(paths)
572 context = _ScanContext(resolved_root, unsupported, active_tools, tracked_paths)
574 _scan_one_path(inventory, context, rel)
576 _scan_repository_governance(inventory, resolved_root, paths)
577 records, findings = scan_baseline_repository(
578 resolved_root, paths, enforce_floors=enforce_floors
580 inventory.suppressions.extend(records)
581 inventory.findings.extend(findings)
582 deduplicate(inventory)
583 validate_fingerprints(inventory)
584 validate_regions(inventory)
585 validate_cppcheck_anchors(inventory, resolved_root)
586 assign_identities(inventory, resolved_root)
587 _append_unsupported(inventory, unsupported)
589 apply_ledger(inventory, resolved_root)
590 _governance_count_contracts(inventory)
591 _nonvacuity(inventory)
595def scan_repository(root: Path) -> tuple[Inventory, list[str]]:
596 """Enumerate and scan the live repository with non-vacuity guards."""
597 paths, findings = git_paths(root)
598 inventory = scan_paths(root, paths, enforce_floors=
True)
if paths
else Inventory()
599 inventory.findings.extend(findings)
600 return inventory, paths