secure-code-agent 0.12.7__tar.gz → 0.12.9__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 (53) hide show
  1. {secure_code_agent-0.12.7/src/secure_code_agent.egg-info → secure_code_agent-0.12.9}/PKG-INFO +6 -4
  2. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/README.md +5 -3
  3. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9/src/secure_code_agent.egg-info}/PKG-INFO +6 -4
  4. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/__init__.py +1 -1
  5. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/cli.py +29 -0
  6. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/config.py +6 -0
  7. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/remediation.py +88 -0
  8. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/triage.py +20 -1
  9. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/LICENSE +0 -0
  10. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/pyproject.toml +0 -0
  11. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/setup.cfg +0 -0
  12. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_agent.egg-info/SOURCES.txt +0 -0
  13. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
  14. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
  15. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_agent.egg-info/requires.txt +0 -0
  16. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_agent.egg-info/top_level.txt +0 -0
  17. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/baseline.py +0 -0
  18. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/capabilities.py +0 -0
  19. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
  20. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/demo.py +0 -0
  21. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/findings.py +0 -0
  22. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/git_tools.py +0 -0
  23. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/history.py +0 -0
  24. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/instructions.py +0 -0
  25. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/mcp_server.py +0 -0
  26. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/pillar.py +0 -0
  27. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/practice.py +0 -0
  28. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/renderers.py +0 -0
  29. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/ruleset.py +0 -0
  30. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/sarif.py +0 -0
  31. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanner_status.py +0 -0
  32. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/__init__.py +0 -0
  33. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
  34. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/base.py +0 -0
  35. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
  36. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
  37. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/floor.py +0 -0
  38. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
  39. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
  40. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
  41. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
  42. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
  43. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
  44. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
  45. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
  46. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
  47. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
  48. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
  49. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
  50. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/scoring.py +0 -0
  51. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/standards.py +0 -0
  52. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/src/secure_code_audit/suppressions.py +0 -0
  53. {secure_code_agent-0.12.7 → secure_code_agent-0.12.9}/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.7
3
+ Version: 0.12.9
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
@@ -60,7 +60,9 @@ secure-code-agent --fail-on-gate \
60
60
  --sarif-output secure-code.sarif
61
61
  ```
62
62
 
63
- The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Same shape: deterministic CI gate · plain-file outputs · per-host skill bundle. Different concern: security, not maintainability.
63
+ **Runs on its own.** Install it, point it at a repository, get the gate, the report and the work order. Nothing else is required, and most people who use it will use only this.
64
+
65
+ It is also the sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — same shape (deterministic CI gate · plain-file outputs · per-host skill bundle), different concern — and can feed that tool's Security pillar through [one file](docs/ma-integration.md). That is an integration, not a dependency: a family resemblance, not a component relationship. The two are separate products, installed separately, with their own dependency pins.
64
66
 
65
67
  ---
66
68
 
@@ -513,7 +515,7 @@ For agents that support invokable skills, this repo ships a portable skill under
513
515
  ## GitHub Action
514
516
 
515
517
  ```yaml
516
- - uses: marshallguillory86/secure-code-agent@v0.12.7
518
+ - uses: marshallguillory86/secure-code-agent@v0.12.9
517
519
  with:
518
520
  config: secure-code-agent.json
519
521
  fail-on-gate: true
@@ -612,4 +614,4 @@ MIT — see [`LICENSE`](LICENSE).
612
614
 
613
615
  ---
614
616
 
615
- Built by [Marshall Guillory](https://github.com/marshallguillory86). The companion to [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — both tools encode a single thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
617
+ Built by [Marshall Guillory](https://github.com/marshallguillory86), who also builds [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Two independent tools, released and installed separately, encoding one thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
@@ -12,7 +12,9 @@ secure-code-agent --fail-on-gate \
12
12
  --sarif-output secure-code.sarif
13
13
  ```
14
14
 
15
- The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Same shape: deterministic CI gate · plain-file outputs · per-host skill bundle. Different concern: security, not maintainability.
15
+ **Runs on its own.** Install it, point it at a repository, get the gate, the report and the work order. Nothing else is required, and most people who use it will use only this.
16
+
17
+ It is also the sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — same shape (deterministic CI gate · plain-file outputs · per-host skill bundle), different concern — and can feed that tool's Security pillar through [one file](docs/ma-integration.md). That is an integration, not a dependency: a family resemblance, not a component relationship. The two are separate products, installed separately, with their own dependency pins.
16
18
 
17
19
  ---
18
20
 
@@ -465,7 +467,7 @@ For agents that support invokable skills, this repo ships a portable skill under
465
467
  ## GitHub Action
466
468
 
467
469
  ```yaml
468
- - uses: marshallguillory86/secure-code-agent@v0.12.7
470
+ - uses: marshallguillory86/secure-code-agent@v0.12.9
469
471
  with:
470
472
  config: secure-code-agent.json
471
473
  fail-on-gate: true
@@ -564,4 +566,4 @@ MIT — see [`LICENSE`](LICENSE).
564
566
 
565
567
  ---
566
568
 
567
- Built by [Marshall Guillory](https://github.com/marshallguillory86). The companion to [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — both tools encode a single thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
569
+ Built by [Marshall Guillory](https://github.com/marshallguillory86), who also builds [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Two independent tools, released and installed separately, encoding one thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secure-code-agent
3
- Version: 0.12.7
3
+ Version: 0.12.9
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
@@ -60,7 +60,9 @@ secure-code-agent --fail-on-gate \
60
60
  --sarif-output secure-code.sarif
61
61
  ```
62
62
 
63
- The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Same shape: deterministic CI gate · plain-file outputs · per-host skill bundle. Different concern: security, not maintainability.
63
+ **Runs on its own.** Install it, point it at a repository, get the gate, the report and the work order. Nothing else is required, and most people who use it will use only this.
64
+
65
+ It is also the sibling of [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — same shape (deterministic CI gate · plain-file outputs · per-host skill bundle), different concern — and can feed that tool's Security pillar through [one file](docs/ma-integration.md). That is an integration, not a dependency: a family resemblance, not a component relationship. The two are separate products, installed separately, with their own dependency pins.
64
66
 
65
67
  ---
66
68
 
@@ -513,7 +515,7 @@ For agents that support invokable skills, this repo ships a portable skill under
513
515
  ## GitHub Action
514
516
 
515
517
  ```yaml
516
- - uses: marshallguillory86/secure-code-agent@v0.12.7
518
+ - uses: marshallguillory86/secure-code-agent@v0.12.9
517
519
  with:
518
520
  config: secure-code-agent.json
519
521
  fail-on-gate: true
@@ -612,4 +614,4 @@ MIT — see [`LICENSE`](LICENSE).
612
614
 
613
615
  ---
614
616
 
615
- Built by [Marshall Guillory](https://github.com/marshallguillory86). The companion to [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent) — both tools encode a single thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
617
+ Built by [Marshall Guillory](https://github.com/marshallguillory86), who also builds [`maintainability-agent`](https://github.com/marshallguillory86/maintainability-agent). Two independent tools, released and installed separately, encoding one thesis: *AI agents need deterministic boundaries, not best-effort guardrails.*
@@ -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.7"
15
+ __version__ = "0.12.9"
@@ -98,6 +98,14 @@ def _parser() -> argparse.ArgumentParser:
98
98
  )
99
99
  p.add_argument("--comment-output", help="PR-comment markdown output path.")
100
100
  p.add_argument("--prompt-output", help="Remediation prompt output path.")
101
+ p.add_argument(
102
+ "--work-order-json",
103
+ help=(
104
+ "The remediation work order as JSON: the same triage and the same "
105
+ "caps as the prompt, as facts rather than prose, for a tool that "
106
+ "renders its own presentation."
107
+ ),
108
+ )
101
109
  p.add_argument(
102
110
  "--security-pillar",
103
111
  help=(
@@ -878,6 +886,10 @@ def _own_artifacts(paths: _OutputPaths, cfg: config_mod.Config, root: Path) -> f
878
886
  paths.json_out,
879
887
  paths.sarif,
880
888
  paths.code_scanning_sarif,
889
+ # The work order as data (D27). Here for the same reason the prompt
890
+ # is: it quotes finding text and code snippets, so the next run
891
+ # would score this tool's own output as source.
892
+ paths.work_order_json,
881
893
  paths.comment,
882
894
  paths.prompt,
883
895
  paths.security_pillar,
@@ -1030,6 +1042,20 @@ def _write_outputs(
1030
1042
  # The work order needs the axis to tier a finding: a secret in a
1031
1043
  # test fixture is a suppression decision, not a patch target.
1032
1044
  remediation.write(findings, paths.prompt, root, lambda f: renderers.axis_of(f, axes))
1045
+ # The same work order as facts. `maintainability-agent` embeds this one
1046
+ # in an HTML report, and prose is the thing a consumer cannot
1047
+ # re-present: with only Markdown it wrapped the text in `<pre>` and a
1048
+ # reader got `##` headings inside a rendered page (D27).
1049
+ if paths.work_order_json is not None:
1050
+ paths.work_order_json.parent.mkdir(parents=True, exist_ok=True)
1051
+ paths.work_order_json.write_text(
1052
+ json.dumps(
1053
+ remediation.as_data(findings, root, lambda f: renderers.axis_of(f, axes)),
1054
+ indent=2,
1055
+ )
1056
+ + "\n",
1057
+ encoding="utf-8",
1058
+ )
1033
1059
  # The artifact maintainability-agent ingests (D3). Written last because it
1034
1060
  # is the only output that carries both axes plus the practice level.
1035
1061
  if paths.security_pillar is not None and pillar is not None:
@@ -1062,6 +1088,8 @@ class _OutputPaths:
1062
1088
  prompt: Path | None
1063
1089
  security_pillar: Path | None
1064
1090
  code_scanning_sarif: Path | None = None
1091
+ #: The work order as data, for a tool that renders its own (D27).
1092
+ work_order_json: Path | None = None
1065
1093
 
1066
1094
 
1067
1095
  def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Path) -> _OutputPaths:
@@ -1107,6 +1135,7 @@ def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Pat
1107
1135
  prompt=_p(args.prompt_output, "prompt_path"),
1108
1136
  security_pillar=_p(args.security_pillar, "security_pillar_path"),
1109
1137
  code_scanning_sarif=_p(args.code_scanning_sarif_output, "code_scanning_sarif_path"),
1138
+ work_order_json=_p(args.work_order_json, "work_order_json_path"),
1110
1139
  )
1111
1140
 
1112
1141
 
@@ -147,6 +147,12 @@ DEFAULT_OUTPUTS: dict[str, str] = {
147
147
  "code_scanning_sarif_path": "secure-code.code-scanning.sarif",
148
148
  "comment_path": "secure-code-pr-comment.md",
149
149
  "prompt_path": "secure-code-remediation-prompt.md",
150
+ # The work order as data, for a tool that renders its own presentation
151
+ # (D27). Off unless declared, like the code-scanning SARIF: the prompt
152
+ # is the artifact an operator reads, and a second copy in JSON is only
153
+ # wanted by a consumer. The name is still declared so this tool never
154
+ # scans a file it wrote.
155
+ "work_order_json_path": "secure-code-work-order.json",
150
156
  "baseline_path": "secure-code-baseline.json",
151
157
  # Append-only trend. The score's one genuine use is movement over time.
152
158
  "history_path": ".secure-code/history.jsonl",
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  from collections.abc import Iterable
12
12
  from pathlib import Path
13
+ from typing import Any
13
14
 
14
15
  from secure_code_audit import triage
15
16
  from secure_code_audit.findings import Finding
@@ -245,6 +246,93 @@ def generate(
245
246
  return "\n".join(parts)
246
247
 
247
248
 
249
+ #: The work order's data schema. Bumped when a consumer would have to
250
+ #: change to keep reading it; new optional keys do not bump it.
251
+ WORK_ORDER_SCHEMA_VERSION = 1
252
+
253
+
254
+ def _finding_data(f: Finding, root: Path | None, note: str | None = None) -> dict[str, Any]:
255
+ """One finding, as the facts a renderer needs and nothing more.
256
+
257
+ Deliberately not the whole `Finding`. The fingerprint, the baseline
258
+ flag and the suppression note are this tool's bookkeeping; a consumer
259
+ drawing a work order needs what it is, where it is, what to do, and
260
+ the citation that makes it checkable.
261
+ """
262
+ return {
263
+ "rule_id": f.rule_id,
264
+ "scanner": f.scanner,
265
+ "title": f.short_desc or f.message,
266
+ "message": f.message,
267
+ "fix_hint": f.fix_hint,
268
+ "severity": f.severity.value,
269
+ "confidence": f.confidence.value,
270
+ "category": f.category.value,
271
+ "path": _display_path(f.file_path, root),
272
+ "line_start": f.line_start,
273
+ "line_end": f.line_end,
274
+ "location": _location(f, root),
275
+ "code_snippet": f.code_snippet,
276
+ "standards": {
277
+ "cwe": f.canonical_cwe,
278
+ "cwe_top25": f.cwe_top25,
279
+ "owasp_top10": f.owasp_top10,
280
+ "asvs_section": f.asvs_section,
281
+ "nist_ssdf": f.nist_ssdf,
282
+ },
283
+ # Only §REVIEW findings carry one: it is why the tier demoted them.
284
+ "review_note": note,
285
+ }
286
+
287
+
288
+ def as_data(
289
+ findings: Iterable[Finding],
290
+ root: Path | None = None,
291
+ axis_of=lambda _f: "primary",
292
+ ) -> dict[str, Any]:
293
+ """The work order as facts, for a consumer that renders its own.
294
+
295
+ `generate` writes Markdown, which is right for the operator and for
296
+ the agent that reads the prompt. It is the wrong thing to hand a
297
+ *tool*: `maintainability-agent` embeds this work order in an HTML
298
+ report and had only prose, so it wrapped it in `<pre>` and a reader
299
+ got raw Markdown — headings as `##`, bold as asterisks — inside a
300
+ page where everything else was rendered.
301
+
302
+ Prose is the one thing a consumer cannot re-present. This is the same
303
+ triage, the same order and the same caps, as data: the tiers come from
304
+ `triage.partition` exactly as the Markdown's do, so the two can never
305
+ describe different work.
306
+
307
+ The caps are kept rather than dropped. A consumer showing more than
308
+ the prompt shows would disagree with the artifact the operator reads,
309
+ and `omitted` says how many are not here — the same number
310
+ `_overflow` prints.
311
+ """
312
+ tiers = triage.partition(findings, axis_of, root)
313
+ fix = tiers[triage.Tier.FIX]
314
+ review = tiers[triage.Tier.REVIEW]
315
+ accept = tiers[triage.Tier.ACCEPT]
316
+ return {
317
+ "schema_version": WORK_ORDER_SCHEMA_VERSION,
318
+ "counts": {"fix": len(fix), "review": len(review), "accept": len(accept)},
319
+ "fix": {
320
+ "shown": [_finding_data(f, root) for f in fix[:_MAX_BLOCKS]],
321
+ "omitted": max(0, len(fix) - _MAX_BLOCKS),
322
+ },
323
+ "review": {
324
+ "shown": [
325
+ _finding_data(f, root, triage.reason_for(f, root))
326
+ for f in review[:_MAX_REVIEW_LINES]
327
+ ],
328
+ "omitted": max(0, len(review) - _MAX_REVIEW_LINES),
329
+ },
330
+ # The accept tier is a count and a summary in the Markdown too: it is
331
+ # the test tree and documentation, which nobody patches one by one.
332
+ "accept": {"summary": _accept_summary(accept, root) if accept else ""},
333
+ }
334
+
335
+
248
336
  def _location(f: Finding, root: Path | None) -> str:
249
337
  span = f"-{f.line_end}" if f.line_end and f.line_end != f.line_start else ""
250
338
  return f"{_display_path(f.file_path, root)}:{f.line_start}{span}"
@@ -30,6 +30,7 @@ import re
30
30
  from collections.abc import Iterable
31
31
  from pathlib import Path
32
32
 
33
+ from secure_code_audit.capabilities import AXIS_PREFIX
33
34
  from secure_code_audit.findings import Category, Confidence, Finding, Severity
34
35
 
35
36
 
@@ -75,6 +76,24 @@ LOW_PRECISION: dict[str, str] = {
75
76
  _ACCEPT_AXES = frozenset({"test tree", "documentation"})
76
77
 
77
78
 
79
+ def _is_accepted_axis(axis: str) -> bool:
80
+ """Is this an axis whose findings are not patch targets?
81
+
82
+ The named pair, plus every declared capability. A declaration says the
83
+ finding is the project's architecture being exercised (D24) — the
84
+ scorer already routes it off the code condition and the report already
85
+ lists it as reported-not-scored. Nominating the same finding in §FIX
86
+ asks an agent to change the thing the operator declared.
87
+
88
+ That is what shipped. `capabilities` reached the scorer and the
89
+ renderer and never reached here, so one run said "no findings" in the
90
+ pillar and "68 to fix" in the work order — the same 68, all of them
91
+ `subprocess` calls in a tool whose purpose is running subprocesses
92
+ (D28).
93
+ """
94
+ return axis in _ACCEPT_AXES or axis.startswith(AXIS_PREFIX)
95
+
96
+
78
97
  #: Bandit quotes the offending value: `Possible hardcoded password: 'x'`.
79
98
  _QUOTED_VALUE = re.compile(r"password: '(.*)'\s*$")
80
99
 
@@ -233,7 +252,7 @@ def tier_of(finding: Finding, axis: str = "primary", root: Path | None = None) -
233
252
  genuine hardcoded key in a test fixture is still deliberate, and a
234
253
  high-confidence rule firing there does not make it a patch target.
235
254
  """
236
- if axis in _ACCEPT_AXES:
255
+ if _is_accepted_axis(axis):
237
256
  return Tier.ACCEPT
238
257
  if finding.rule_id in LOW_PRECISION:
239
258
  # Unless the value itself gives it away.