secure-code-agent 0.12.2__tar.gz → 0.12.4__tar.gz

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.
Files changed (52) hide show
  1. {secure_code_agent-0.12.2/src/secure_code_agent.egg-info → secure_code_agent-0.12.4}/PKG-INFO +13 -3
  2. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/README.md +12 -2
  3. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4/src/secure_code_agent.egg-info}/PKG-INFO +13 -3
  4. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/__init__.py +1 -1
  5. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/cli.py +22 -0
  6. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/config.py +4 -0
  7. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/git_tools.py +11 -0
  8. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/sarif.py +36 -5
  9. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/suppressions.py +10 -2
  10. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/LICENSE +0 -0
  11. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/pyproject.toml +0 -0
  12. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/setup.cfg +0 -0
  13. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_agent.egg-info/SOURCES.txt +0 -0
  14. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
  15. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
  16. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_agent.egg-info/requires.txt +0 -0
  17. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_agent.egg-info/top_level.txt +0 -0
  18. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/baseline.py +0 -0
  19. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
  20. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/demo.py +0 -0
  21. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/findings.py +0 -0
  22. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/history.py +0 -0
  23. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/instructions.py +0 -0
  24. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/mcp_server.py +0 -0
  25. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/pillar.py +0 -0
  26. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/practice.py +0 -0
  27. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/remediation.py +0 -0
  28. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/renderers.py +0 -0
  29. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/ruleset.py +0 -0
  30. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanner_status.py +0 -0
  31. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/__init__.py +0 -0
  32. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
  33. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/base.py +0 -0
  34. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
  35. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
  36. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/floor.py +0 -0
  37. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
  38. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
  39. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
  40. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
  41. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
  42. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
  43. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
  44. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
  45. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
  46. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
  47. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
  48. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
  49. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/scoring.py +0 -0
  50. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/standards.py +0 -0
  51. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/triage.py +0 -0
  52. {secure_code_agent-0.12.2 → secure_code_agent-0.12.4}/src/secure_code_audit/verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secure-code-agent
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Deterministic security gate + bounded AI remediation prompt generator. NIST SSDF / OWASP ASVS / CWE Top 25 anchored.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -448,6 +448,8 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
448
448
  expires: "2027-05-13"
449
449
  ```
450
450
 
451
+ `paths` entries are repository-relative and use the same pattern syntax as `exclude_patterns`: a trailing `/` means that directory at any depth, a bare name matches at any depth, and `**/` includes the root.
452
+
451
453
  Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
452
454
 
453
455
  ## Baseline + incremental adoption
@@ -511,7 +513,7 @@ For agents that support invokable skills, this repo ships a portable skill under
511
513
  ## GitHub Action
512
514
 
513
515
  ```yaml
514
- - uses: marshallguillory86/secure-code-agent@v0.12.2
516
+ - uses: marshallguillory86/secure-code-agent@v0.12.4
515
517
  with:
516
518
  config: secure-code-agent.json
517
519
  fail-on-gate: true
@@ -521,7 +523,15 @@ The action installs the exact source bundled with the referenced action plus
521
523
  the pinned `required-scanners` extra (Bandit and pip-audit),
522
524
  emits Markdown, JSON, SARIF, PR-comment, and remediation artifacts, and uploads
523
525
  SARIF by default. The calling workflow must grant `security-events: write` for
524
- SARIF upload. Pin production usage to a full commit SHA; the version tag above
526
+ SARIF upload.
527
+
528
+ What it uploads is `secure-code.code-scanning.sarif`, not the full SARIF. Code
529
+ scanning opens an alert for every uploaded result and does not act on SARIF
530
+ `suppressions`, so uploading the full file raises an alert, and a review thread
531
+ on pull requests, for every test-tree and documentation finding and every
532
+ reviewed `.scignore.yaml` entry. The upload file leaves those out; the full SARIF,
533
+ with each suppression and its reason, is kept as an artifact. Outside the action,
534
+ `--code-scanning-sarif-output PATH` writes the same file. Pin production usage to a full commit SHA; the version tag above
525
535
  is shown for readability. See [`action.yml`](action.yml) and
526
536
  [`examples/github-actions/`](examples/github-actions/) for full workflows.
527
537
 
@@ -400,6 +400,8 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
400
400
  expires: "2027-05-13"
401
401
  ```
402
402
 
403
+ `paths` entries are repository-relative and use the same pattern syntax as `exclude_patterns`: a trailing `/` means that directory at any depth, a bare name matches at any depth, and `**/` includes the root.
404
+
403
405
  Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
404
406
 
405
407
  ## Baseline + incremental adoption
@@ -463,7 +465,7 @@ For agents that support invokable skills, this repo ships a portable skill under
463
465
  ## GitHub Action
464
466
 
465
467
  ```yaml
466
- - uses: marshallguillory86/secure-code-agent@v0.12.2
468
+ - uses: marshallguillory86/secure-code-agent@v0.12.4
467
469
  with:
468
470
  config: secure-code-agent.json
469
471
  fail-on-gate: true
@@ -473,7 +475,15 @@ The action installs the exact source bundled with the referenced action plus
473
475
  the pinned `required-scanners` extra (Bandit and pip-audit),
474
476
  emits Markdown, JSON, SARIF, PR-comment, and remediation artifacts, and uploads
475
477
  SARIF by default. The calling workflow must grant `security-events: write` for
476
- SARIF upload. Pin production usage to a full commit SHA; the version tag above
478
+ SARIF upload.
479
+
480
+ What it uploads is `secure-code.code-scanning.sarif`, not the full SARIF. Code
481
+ scanning opens an alert for every uploaded result and does not act on SARIF
482
+ `suppressions`, so uploading the full file raises an alert, and a review thread
483
+ on pull requests, for every test-tree and documentation finding and every
484
+ reviewed `.scignore.yaml` entry. The upload file leaves those out; the full SARIF,
485
+ with each suppression and its reason, is kept as an artifact. Outside the action,
486
+ `--code-scanning-sarif-output PATH` writes the same file. Pin production usage to a full commit SHA; the version tag above
477
487
  is shown for readability. See [`action.yml`](action.yml) and
478
488
  [`examples/github-actions/`](examples/github-actions/) for full workflows.
479
489
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secure-code-agent
3
- Version: 0.12.2
3
+ Version: 0.12.4
4
4
  Summary: Deterministic security gate + bounded AI remediation prompt generator. NIST SSDF / OWASP ASVS / CWE Top 25 anchored.
5
5
  Author: Marshall Guillory
6
6
  License: MIT
@@ -448,6 +448,8 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
448
448
  expires: "2027-05-13"
449
449
  ```
450
450
 
451
+ `paths` entries are repository-relative and use the same pattern syntax as `exclude_patterns`: a trailing `/` means that directory at any depth, a bare name matches at any depth, and `**/` includes the root.
452
+
451
453
  Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
452
454
 
453
455
  ## Baseline + incremental adoption
@@ -511,7 +513,7 @@ For agents that support invokable skills, this repo ships a portable skill under
511
513
  ## GitHub Action
512
514
 
513
515
  ```yaml
514
- - uses: marshallguillory86/secure-code-agent@v0.12.2
516
+ - uses: marshallguillory86/secure-code-agent@v0.12.4
515
517
  with:
516
518
  config: secure-code-agent.json
517
519
  fail-on-gate: true
@@ -521,7 +523,15 @@ The action installs the exact source bundled with the referenced action plus
521
523
  the pinned `required-scanners` extra (Bandit and pip-audit),
522
524
  emits Markdown, JSON, SARIF, PR-comment, and remediation artifacts, and uploads
523
525
  SARIF by default. The calling workflow must grant `security-events: write` for
524
- SARIF upload. Pin production usage to a full commit SHA; the version tag above
526
+ SARIF upload.
527
+
528
+ What it uploads is `secure-code.code-scanning.sarif`, not the full SARIF. Code
529
+ scanning opens an alert for every uploaded result and does not act on SARIF
530
+ `suppressions`, so uploading the full file raises an alert, and a review thread
531
+ on pull requests, for every test-tree and documentation finding and every
532
+ reviewed `.scignore.yaml` entry. The upload file leaves those out; the full SARIF,
533
+ with each suppression and its reason, is kept as an artifact. Outside the action,
534
+ `--code-scanning-sarif-output PATH` writes the same file. Pin production usage to a full commit SHA; the version tag above
525
535
  is shown for readability. See [`action.yml`](action.yml) and
526
536
  [`examples/github-actions/`](examples/github-actions/) for full workflows.
527
537
 
@@ -12,4 +12,4 @@
12
12
  #:
13
13
  #: PyPI is immutable, so 0.4.0 stays wrong. 0.5.0 is the first build whose
14
14
  #: artifacts name their own producer correctly.
15
- __version__ = "0.12.2"
15
+ __version__ = "0.12.4"
@@ -82,6 +82,15 @@ def _parser() -> argparse.ArgumentParser:
82
82
  p.add_argument("--output", help="Markdown report output path.")
83
83
  p.add_argument("--json-output", help="Canonical JSON output path.")
84
84
  p.add_argument("--sarif-output", help="SARIF 2.1.0 output path.")
85
+ p.add_argument(
86
+ "--code-scanning-sarif-output",
87
+ help=(
88
+ "SARIF for upload to GitHub code scanning: the --sarif-output results "
89
+ "without the suppressed ones (test tree, documentation, .scignore.yaml). "
90
+ "Code scanning opens an alert for every uploaded result and ignores "
91
+ "SARIF suppressions, so upload this file, not the record."
92
+ ),
93
+ )
85
94
  p.add_argument("--comment-output", help="PR-comment markdown output path.")
86
95
  p.add_argument("--prompt-output", help="Remediation prompt output path.")
87
96
  p.add_argument(
@@ -795,6 +804,7 @@ def _own_artifacts(paths: _OutputPaths, cfg: config_mod.Config, root: Path) -> f
795
804
  paths.markdown,
796
805
  paths.json_out,
797
806
  paths.sarif,
807
+ paths.code_scanning_sarif,
798
808
  paths.comment,
799
809
  paths.prompt,
800
810
  paths.security_pillar,
@@ -933,6 +943,14 @@ def _write_outputs(
933
943
  renderers.write_json(findings, score, gate, paths.json_out, coverage, verdict, axes, root)
934
944
  if paths.sarif is not None:
935
945
  sarif.write(findings, paths.sarif, coverage, lambda f: renderers.axis_of(f, axes))
946
+ if paths.code_scanning_sarif is not None:
947
+ sarif.write(
948
+ findings,
949
+ paths.code_scanning_sarif,
950
+ coverage,
951
+ lambda f: renderers.axis_of(f, axes),
952
+ alerts_only=True,
953
+ )
936
954
  if paths.comment is not None:
937
955
  renderers.write_pr_comment(findings, score, gate, paths.comment, coverage, verdict)
938
956
  if paths.prompt is not None:
@@ -970,6 +988,7 @@ class _OutputPaths:
970
988
  comment: Path | None
971
989
  prompt: Path | None
972
990
  security_pillar: Path | None
991
+ code_scanning_sarif: Path | None = None
973
992
 
974
993
 
975
994
  def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Path) -> _OutputPaths:
@@ -1014,6 +1033,7 @@ def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Pat
1014
1033
  comment=_p(args.comment_output, "comment_path"),
1015
1034
  prompt=_p(args.prompt_output, "prompt_path"),
1016
1035
  security_pillar=_p(args.security_pillar, "security_pillar_path"),
1036
+ code_scanning_sarif=_p(args.code_scanning_sarif_output, "code_scanning_sarif_path"),
1017
1037
  )
1018
1038
 
1019
1039
 
@@ -1027,6 +1047,7 @@ _WRITE_KEYS = (
1027
1047
  "markdown_path",
1028
1048
  "json_path",
1029
1049
  "sarif_path",
1050
+ "code_scanning_sarif_path",
1030
1051
  "comment_path",
1031
1052
  "prompt_path",
1032
1053
  "security_pillar_path",
@@ -1169,6 +1190,7 @@ def _print_summary(
1169
1190
  paths.markdown,
1170
1191
  paths.json_out,
1171
1192
  paths.sarif,
1193
+ paths.code_scanning_sarif,
1172
1194
  paths.comment,
1173
1195
  paths.prompt,
1174
1196
  paths.security_pillar,
@@ -136,6 +136,10 @@ DEFAULT_OUTPUTS: dict[str, str] = {
136
136
  "markdown_path": "secure-code-report.md",
137
137
  "json_path": "secure-code-report.json",
138
138
  "sarif_path": "secure-code.sarif",
139
+ # For upload to code scanning: the SARIF above without results that carry
140
+ # `suppressions`, which GitHub turns into alerts regardless (D22). Off
141
+ # unless declared, like sarif_path.
142
+ "code_scanning_sarif_path": "secure-code.code-scanning.sarif",
139
143
  "comment_path": "secure-code-pr-comment.md",
140
144
  "prompt_path": "secure-code-remediation-prompt.md",
141
145
  "baseline_path": "secure-code-baseline.json",
@@ -48,6 +48,17 @@ def is_excluded(path: Path, root: Path, patterns: Iterable[str]) -> bool:
48
48
  return any(_matches(rel, path.name, pat) for pat in patterns)
49
49
 
50
50
 
51
+ def matches_pattern(rel: str, pattern: str) -> bool:
52
+ """One configured path pattern against one repository-relative POSIX path.
53
+
54
+ The pattern language every path setting shares: `exclude_patterns`,
55
+ `test_patterns`, `docs_patterns`, and `.scignore.yaml` `paths:`. There is
56
+ one of it so the same string cannot mean two things in one configuration
57
+ (D21).
58
+ """
59
+ return _matches(rel, rel.rsplit("/", 1)[-1], pattern)
60
+
61
+
51
62
  def _matches(rel: str, name: str, pat: str) -> bool:
52
63
  """One glob against one path, already made relative to the root.
53
64
 
@@ -18,6 +18,7 @@ from secure_code_audit.scanner_status import (
18
18
  ScannerExecution,
19
19
  ScannerOutcome,
20
20
  )
21
+ from secure_code_audit.scoring import GATED_FROM_ANY_AXIS
21
22
  from secure_code_audit.standards import cwe_url
22
23
 
23
24
  _SARIF_LEVEL = {
@@ -33,8 +34,25 @@ def emit(
33
34
  findings: Iterable[Finding],
34
35
  coverage: CoverageReport | None = None,
35
36
  axis_of=lambda _f: "primary",
37
+ *,
38
+ alerts_only: bool = False,
36
39
  ) -> dict:
37
- """Build a SARIF 2.1.0 document from canonical findings."""
40
+ """Build a SARIF 2.1.0 document from canonical findings.
41
+
42
+ `alerts_only` builds the document for upload to a code-scanning platform:
43
+ results carrying `suppressions` are left out, rather than kept with the
44
+ field. GitHub code scanning opens an alert for every uploaded result and
45
+ does not act on `suppressions`, so the standard form turned each
46
+ test-tree `assert` into a pull-request review thread once D20 made paths
47
+ placeable (D22). The default keeps every result, as the record.
48
+
49
+ **Except what the build fails on.** A side-axis finding in a category the
50
+ gate escalates from any axis — a secret in a test fixture — is marked
51
+ suppressed in the record and still uploaded, without the marker, so the
52
+ Security tab shows the finding that failed the build. An operator's
53
+ reviewed suppression is left out whatever its category; it returns as a
54
+ live critical finding when it expires.
55
+ """
38
56
  findings = list(findings)
39
57
 
40
58
  # Build the rules array per-canonical-rule (rule_id is local; dedupe).
@@ -42,10 +60,15 @@ def emit(
42
60
  results: list[dict] = []
43
61
 
44
62
  for f in findings:
63
+ result = _result(f, axis_of(f))
64
+ if alerts_only and result.get("suppressions"):
65
+ if f.suppressed or f.category not in GATED_FROM_ANY_AXIS:
66
+ continue
67
+ del result["suppressions"]
45
68
  rid = f.rule_id
46
69
  if rid not in rule_meta:
47
70
  rule_meta[rid] = _rule(f)
48
- results.append(_result(f, axis_of(f)))
71
+ results.append(result)
49
72
 
50
73
  run = {
51
74
  "tool": {
@@ -144,8 +167,13 @@ def _suppressions(f: Finding, axis: str) -> list[dict] | None:
144
167
  the shipped source.
145
168
 
146
169
  Suppressed, not omitted. The finding stays in the file with its
147
- location and justification, so nothing is hidden from a reader — it
148
- simply does not become someone's ticket.
170
+ location and justification, so nothing is hidden from a reader.
171
+
172
+ **GitHub code scanning does not honour it (D22).** It opens an alert for
173
+ every uploaded result regardless, which this function's premise assumed
174
+ it would not. So this is right for the record, and not enough for the
175
+ upload: `emit(..., alerts_only=True)` leaves these results out of the
176
+ file that goes to code scanning.
149
177
  """
150
178
  if f.suppressed:
151
179
  return [
@@ -206,8 +234,11 @@ def write(
206
234
  output: Path,
207
235
  coverage: CoverageReport | None = None,
208
236
  axis_of=lambda _f: "primary",
237
+ *,
238
+ alerts_only: bool = False,
209
239
  ) -> None:
210
- output.write_text(json.dumps(emit(findings, coverage, axis_of), indent=2), encoding="utf-8")
240
+ document = emit(findings, coverage, axis_of, alerts_only=alerts_only)
241
+ output.write_text(json.dumps(document, indent=2), encoding="utf-8")
211
242
 
212
243
 
213
244
  # ----- ingest -------------------------------------------------------------
@@ -17,7 +17,7 @@ Entry schema (.scignore.yaml)::
17
17
  ...
18
18
  expires: 2027-08-01 # required; max 365 days out
19
19
  file: api/tests/x.py # optional path (suffix match)
20
- paths: ["api/**"] # optional fnmatch patterns, repository-relative
20
+ paths: ["api/"] # optional; exclude_patterns syntax, repository-relative
21
21
  fingerprint: 0aaa689f8a967d8c # optional; 16 hex chars, from the report
22
22
  line: 18 # optional; positive integer
23
23
 
@@ -37,6 +37,7 @@ from dataclasses import dataclass, field
37
37
  from pathlib import Path
38
38
 
39
39
  from secure_code_audit.findings import Category, Confidence, Finding, Severity
40
+ from secure_code_audit.git_tools import matches_pattern
40
41
 
41
42
  _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
42
43
  _MAX_TTL_DAYS = 365
@@ -77,6 +78,13 @@ class SuppressionRule:
77
78
  against the path with a leading `/`, and `*/src/app.py` matches
78
79
  `/src/app.py`. `root` lets an absolute `file:` and a fingerprint
79
80
  recorded by an earlier release keep matching the finding they named.
81
+
82
+ **A `paths:` glob is an exclude pattern (D21).** It is matched by
83
+ `git_tools.matches_pattern`, the matcher `exclude_patterns` uses, so a
84
+ trailing slash means "this directory, at any depth" and a bare name
85
+ matches at any depth. Through 0.12.2 it was bare `fnmatch`, under which
86
+ `paths: ["tests/"]` — the example README, `design.md` and the skill all
87
+ show — matched nothing.
80
88
  """
81
89
  if self.rule_id != "*" and self.rule_id != finding.rule_id:
82
90
  return False
@@ -88,7 +96,7 @@ class SuppressionRule:
88
96
  if self.line is not None and self.line != finding.line_start:
89
97
  return False
90
98
  return not self.paths or any(
91
- fnmatch.fnmatch(rel, p) or fnmatch.fnmatch(f"/{rel}", p) for p in self.paths
99
+ matches_pattern(rel, p) or fnmatch.fnmatch(f"/{rel}", p) for p in self.paths
92
100
  )
93
101
 
94
102
  def _names_file(self, rel: str, finding: Finding, root: Path | None) -> bool: