3"""Typed repository-governance controls that are not ordinary lint comments."""
5from __future__
import annotations
9from dataclasses
import dataclass
10from pathlib
import Path
13from check_gitignore_scope
import marker_bindings
14from suppression_catalog
import ownership
15from suppression_comment_lex
import extract_comments
16from suppression_model
import Finding, Suppression
18CI_DIR = Path(__file__).resolve().parents[1] /
"ci"
19if str(CI_DIR)
not in sys.path:
20 sys.path.insert(0, str(CI_DIR))
21from check_ci_parity
import (
26ANSIBLE_CONFIG_NAMES = frozenset({
".ansible-lint",
".ansible-lint.yml",
".ansible-lint.yaml"})
27ANSIBLE_LIST_KEYS = frozenset({
"skip_list",
"warn_list",
"exclude_paths"})
32GLOBAL_EXCLUSION_AUTHORITIES: dict[str, tuple[str, str]] = {}
37 r"^nosemgrep(?:\s*:\s*(?P<rule>[A-Za-z0-9_.-]+))?"
38 r"(?:\s+--\s+(?P<reason>\S.*))?$",
43 (re.compile(
r"^NOSONAR(?:\s+--\s+(?P<reason>\S.*))?$"),
"sonarqube"),
46 r"^lgtm\[(?P<rule>[A-Za-z0-9_./-]+)\]"
47 r"(?:\s+--\s+(?P<reason>\S.*))?$",
53OTHER_LANGUAGE_RULES = {
57 r'^@SuppressWarnings\(\s*"(?P<rule>[A-Za-z0-9_.-]+)"\s*\)\s*(?://\s*(?P<reason>\S.*))?$'
63 r'^@Suppress\(\s*"(?P<rule>[A-Za-z0-9_.-]+)"\s*\)\s*(?://\s*(?P<reason>\S.*))?$'
69 r"^#\[(?P<kind>allow|expect)\((?P<rule>[A-Za-z0-9_:.-]+)\)\]\s*(?://\s*(?P<reason>\S.*))?$"
74 re.compile(
r"^//nolint:(?P<rule>[A-Za-z0-9_,.-]+)\s+//\s*(?P<reason>\S.*)$"),
79@dataclass(frozen=True)
81 """Normalized fields for one governance control."""
92def _record(path: str, line: int, spec: GovernanceSpec) -> Suppression:
93 """Build one deterministic governance row."""
94 concerns = ()
if spec.reason
else (
"blank-reason",)
111def _ansible_key_records(
117) -> tuple[list[Suppression], list[Finding]]:
118 """Parse one ansible-lint list authority and its item-local reasons."""
119 if not isinstance(values, list):
120 finding = Finding(
"malformed-ansible-lint-config", f
"{key} is not a list", path)
122 records: list[Suppression] = []
123 findings: list[Finding] = []
124 for offset, value
in enumerate(values):
125 if not isinstance(value, str)
or not value.strip():
128 "malformed-ansible-lint-config",
129 f
"{key} has non-string item",
138 for number, raw
in enumerate(lines[key_line - 1 :], start=key_line)
139 if re.match(rf
"^\s*-\s*{re.escape(value)}(?:\s*(?:#.*)?)$", raw)
141 key_line + offset + 1,
143 raw = lines[item_line - 1]
if item_line <= len(lines)
else ""
144 reason = raw.partition(
"#")[2].strip()
145 spec = GovernanceSpec(
146 "ansible-lint-config",
149 key.replace(
"_",
"-"),
154 records.append(_record(path, item_line, spec))
155 return records, findings
158def scan_ansible_lint_config(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
159 """Parse exact ansible-lint list authorities, not task/docs substrings."""
160 if Path(path).name
not in ANSIBLE_CONFIG_NAMES:
163 doc = yaml.safe_load(text)
164 except yaml.YAMLError
as exc:
165 return [], [Finding(
"malformed-ansible-lint-config", str(exc), path)]
166 if not isinstance(doc, dict):
167 return [], [Finding(
"malformed-ansible-lint-config",
"top level is not a mapping", path)]
168 lines = text.splitlines()
169 records: list[Suppression] = []
170 findings: list[Finding] = []
171 for key
in ANSIBLE_LIST_KEYS:
177 for number, raw
in enumerate(lines, start=1)
178 if re.match(rf
"^\s*{key}\s*:", raw)
182 rows, problems = _ansible_key_records(path, key, doc[key], lines, key_line)
184 findings.extend(problems)
185 return records, findings
188def scan_registered_global_exclusions(
189 path: str, _text: str
190) -> tuple[list[Suppression], list[Finding]]:
191 """Parse only explicitly registered non-Ruff exclusion authorities."""
192 authority = GLOBAL_EXCLUSION_AUTHORITIES.get(path)
193 if authority
is None:
195 table, key = authority
199 message = f
"registered parser not implemented for {table}.{key}"
200 return [], [Finding(
"malformed-global-exclusion-config", message, path)]
203def scan_gitignore_exemptions(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
204 """Inventory the exact marker-to-unanchored-pattern bindings the gate consumes."""
205 if path !=
".gitignore":
207 bindings, errors = marker_bindings(text)
215 "unanchored-directory-exemption",
216 "gitignore-scope-ok",
217 f
"pattern:{item.pattern}",
219 "bound-comment-block",
225 Finding(
"malformed-gitignore-scope-marker", message, path, line)
for line, message
in errors
227 return records, findings
230def scan_ci_parity_exemptions(
231 root: Path, paths: list[str]
232) -> tuple[list[Suppression], list[Finding]]:
233 """Inventory active infra run steps through the parity checker's parser."""
234 records: list[Suppression] = []
235 findings: list[Finding] = []
239 if rel.startswith(
".github/workflows/")
and Path(rel).suffix
in {
".yml",
".yaml"}
241 for rel
in workflows:
242 workflow = root / rel
244 text = workflow.read_text(encoding=
"utf-8")
245 steps = list(iter_run_steps(workflow))
246 except (OSError, yaml.YAMLError)
as exc:
247 findings.append(Finding(
"malformed-ci-parity-workflow", str(exc), rel))
250 kind, _gates, reason = classify_step(step.body)
257 for number, raw
in enumerate(text.splitlines(), start=1)
258 if re.match(rf
"^\s*-?\s*name:\s*['\"]?{re.escape(label)}['\"]?\s*$", raw)
267 "ci-parity-exemption",
269 "infrastructure-step",
271 f
"job:{step.job_name}/step:{label}",
277 return records, findings
280def _doxygen_assignments(text: str) -> tuple[list[tuple[int, str, list[str], str]], list[Finding]]:
281 """Return top-level Doxyfile assignments with continuation values/reasons."""
282 rows: list[tuple[int, str, list[str], str]] = []
283 findings: list[Finding] = []
284 lines = text.splitlines()
286 comments: list[str] = []
287 while index < len(lines):
289 stripped = raw.strip()
290 if stripped.startswith(
"#"):
291 comments.append(stripped[1:].strip())
294 match = re.match(
r"^(?P<key>[A-Z][A-Z0-9_]*)\s*(?P<op>\+?=)\s*(?P<value>.*)$", raw)
301 values: list[str] = []
302 part = match.group(
"value").strip()
304 continued = part.endswith(
"\\")
306 part = part[:-1].rstrip()
307 values.extend(part.split())
311 if index >= len(lines):
314 "malformed-doxygen-config",
315 f
"unterminated {match.group('key')}",
321 part = lines[index].strip()
322 reason =
" ".join(item
for item
in comments
if item).strip()
323 rows.append((line_no, match.group(
"key"), values, reason))
326 return rows, findings
329def scan_doxygen_controls(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
330 """Inventory the fatality and exclusion assignments in the root Doxyfile."""
331 if path !=
"Doxyfile":
333 assignments, findings = _doxygen_assignments(text)
335 "WARN_IF_UNDOCUMENTED",
341 records: list[Suppression] = []
342 for line, key, assigned_values, assigned_reason
in assignments:
345 if key.startswith(
"WARN_"):
346 values = assigned_values[:1]
347 reason = assigned_reason
or (
348 "Doxygen warning policy is explicitly configured at repository scope."
351 values = assigned_values
352 reason = assigned_reason
or (
353 "Doxygen excludes non-product, generated, test, or vendored documentation inputs."
356 spec = GovernanceSpec(
357 "documentation-control",
365 records.append(_record(path, line, spec))
366 return records, findings
369def scan_security_controls(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
370 """Recognize exact first-party security-analyzer comment directives."""
371 if ownership(path) !=
"first-party":
373 comments, lex_findings = extract_comments(path, text)
374 records: list[Suppression] = []
375 for comment
in comments:
376 body = comment.text.strip().lstrip(
"*").strip()
377 for pattern, tool
in SECURITY_RULES:
378 match = pattern.fullmatch(body)
381 groups = match.groupdict()
387 "security-analysis-control",
389 groups.get(
"rule")
or "all",
392 groups.get(
"reason")
or "",
398 findings = [Finding(item.code, item.message, path, item.line)
for item
in lex_findings]
399 return records, findings
402def scan_other_language_controls(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
403 """Recognize exact Java/Kotlin/Rust/Go suppression syntax in matching files."""
404 spec = OTHER_LANGUAGE_RULES.get(Path(path).suffix.lower())
405 if spec
is None or ownership(path) !=
"first-party":
408 records: list[Suppression] = []
409 for line, raw
in enumerate(text.splitlines(), start=1):
410 match = pattern.fullmatch(raw.strip())
413 groups = match.groupdict()
419 "other-language-control",
422 groups.get(
"kind")
or "suppress",
424 groups.get(
"reason")
or "",
432def scan_governance_file(path: str, text: str) -> tuple[list[Suppression], list[Finding]]:
433 """Run every typed per-file governance parser."""
434 records: list[Suppression] = []
435 findings: list[Finding] = []
437 scan_ansible_lint_config,
438 scan_registered_global_exclusions,
439 scan_gitignore_exemptions,
440 scan_doxygen_controls,
441 scan_security_controls,
442 scan_other_language_controls,
444 found, problems = parser(path, text)
445 records.extend(found)
446 findings.extend(problems)
447 return records, findings