secure-code-agent 0.12.10__tar.gz → 0.12.11__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.
- {secure_code_agent-0.12.10/src/secure_code_agent.egg-info → secure_code_agent-0.12.11}/PKG-INFO +44 -5
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/README.md +39 -1
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/pyproject.toml +37 -5
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11/src/secure_code_agent.egg-info}/PKG-INFO +44 -5
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/SOURCES.txt +4 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/requires.txt +4 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/__init__.py +1 -1
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/cli.py +226 -79
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/config.py +84 -27
- secure_code_agent-0.12.11/src/secure_code_audit/data/standards.yaml +424 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/findings.py +11 -30
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/git_tools.py +34 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/renderers.py +4 -2
- secure_code_agent-0.12.11/src/secure_code_audit/scanners/_execution.py +112 -0
- secure_code_agent-0.12.11/src/secure_code_audit/scanners/_finding_builder.py +233 -0
- secure_code_agent-0.12.11/src/secure_code_audit/scanners/_resolution.py +157 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/bandit_scanner.py +15 -4
- secure_code_agent-0.12.11/src/secure_code_audit/scanners/base.py +140 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/checkov_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/gitleaks_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/gosec_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/hadolint_scanner.py +1 -1
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/njsscan_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/npm_audit_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/osv_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -1
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/rubocop_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/scorecard_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/semgrep_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/trivy_scanner.py +1 -3
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/trufflehog_scanner.py +1 -1
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scoring.py +13 -2
- secure_code_agent-0.12.11/src/secure_code_audit/standards.py +550 -0
- secure_code_agent-0.12.11/src/secure_code_audit/suppressions.py +442 -0
- secure_code_agent-0.12.10/src/secure_code_audit/scanners/base.py +0 -459
- secure_code_agent-0.12.10/src/secure_code_audit/standards.py +0 -705
- secure_code_agent-0.12.10/src/secure_code_audit/suppressions.py +0 -284
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/LICENSE +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/setup.cfg +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/baseline.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/capabilities.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/demo.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/history.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/mcp_server.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/pillar.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/practice.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/remediation.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/sarif.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanner_status.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/triage.py +0 -0
- {secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_audit/verify.py +0 -0
{secure_code_agent-0.12.10/src/secure_code_agent.egg-info → secure_code_agent-0.12.11}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secure-code-agent
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.11
|
|
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
|
|
@@ -30,11 +30,11 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
Requires-Dist: PyYAML<7,>=6.0.2
|
|
32
32
|
Provides-Extra: mcp
|
|
33
|
-
Requires-Dist: mcp<
|
|
33
|
+
Requires-Dist: mcp<3,>=1.0; extra == "mcp"
|
|
34
34
|
Provides-Extra: required-scanners
|
|
35
35
|
Requires-Dist: bandit==1.9.4; extra == "required-scanners"
|
|
36
36
|
Requires-Dist: pip-audit==2.10.1; extra == "required-scanners"
|
|
37
|
-
Requires-Dist: njsscan==1.0.
|
|
37
|
+
Requires-Dist: njsscan==1.0.1; extra == "required-scanners"
|
|
38
38
|
Provides-Extra: python-scanners
|
|
39
39
|
Requires-Dist: secure-code-agent[required-scanners]; extra == "python-scanners"
|
|
40
40
|
Requires-Dist: semgrep<2,>=1.172; extra == "python-scanners"
|
|
@@ -43,7 +43,8 @@ Provides-Extra: dev
|
|
|
43
43
|
Requires-Dist: secure-code-agent[required-scanners]; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
45
45
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
46
|
-
Requires-Dist: ruff==0.16.
|
|
46
|
+
Requires-Dist: ruff==0.16.9; extra == "dev"
|
|
47
|
+
Requires-Dist: markdown>=3.5; extra == "dev"
|
|
47
48
|
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
# secure-code-agent
|
|
@@ -454,6 +455,44 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
|
|
|
454
455
|
|
|
455
456
|
Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
|
|
456
457
|
|
|
458
|
+
## Extending the standards mapping
|
|
459
|
+
|
|
460
|
+
The shipped mapping table lives in `data/standards.yaml` and covers the rules
|
|
461
|
+
this project curates. Semgrep alone publishes thousands, so point
|
|
462
|
+
`standards_overlay` at your own file to map or correct a rule without waiting
|
|
463
|
+
for a release:
|
|
464
|
+
|
|
465
|
+
```json
|
|
466
|
+
{ "standards_overlay": "security/standards-overlay.yaml" }
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```yaml
|
|
470
|
+
version: 1
|
|
471
|
+
entries:
|
|
472
|
+
- scanner: semgrep
|
|
473
|
+
rule_id: python.lang.security.audit.my-rule
|
|
474
|
+
canonical_cwe: CWE-22
|
|
475
|
+
owasp_top10: A01
|
|
476
|
+
asvs_section: V12.3.1
|
|
477
|
+
nist_ssdf: PW.5.1
|
|
478
|
+
short_desc: Path traversal in our request handler.
|
|
479
|
+
fix_hint: Resolve the path and contain it under the upload root.
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
An overlay entry wins over the shipped table for the fields it sets, and an
|
|
483
|
+
adapter asserting its own CWE still wins over both.
|
|
484
|
+
|
|
485
|
+
**An overlay cannot set `severity`, `confidence` or `category`**, and a file
|
|
486
|
+
that tries is refused with the reason rather than ignored. Those three are the
|
|
487
|
+
scoring inputs: an overlay that set them could move the grade, which is the
|
|
488
|
+
hole [D32](docs/decisions.md) closed on the configuration side. An overlay says
|
|
489
|
+
*what weakness a rule describes*; it does not say *how much it counts*. Set
|
|
490
|
+
severity policy with `severity_overrides` in a config kept outside the audited
|
|
491
|
+
tree.
|
|
492
|
+
|
|
493
|
+
A declared overlay that cannot be read fails the run rather than being skipped,
|
|
494
|
+
for the same reason a malformed `.scignore.yaml` does.
|
|
495
|
+
|
|
457
496
|
## Baseline + incremental adoption
|
|
458
497
|
|
|
459
498
|
`secure-code-baseline.json` fingerprints every current finding. On the next run:
|
|
@@ -515,7 +554,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
515
554
|
## GitHub Action
|
|
516
555
|
|
|
517
556
|
```yaml
|
|
518
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
557
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.11
|
|
519
558
|
with:
|
|
520
559
|
config: secure-code-agent.json
|
|
521
560
|
fail-on-gate: true
|
|
@@ -406,6 +406,44 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
|
|
|
406
406
|
|
|
407
407
|
Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
|
|
408
408
|
|
|
409
|
+
## Extending the standards mapping
|
|
410
|
+
|
|
411
|
+
The shipped mapping table lives in `data/standards.yaml` and covers the rules
|
|
412
|
+
this project curates. Semgrep alone publishes thousands, so point
|
|
413
|
+
`standards_overlay` at your own file to map or correct a rule without waiting
|
|
414
|
+
for a release:
|
|
415
|
+
|
|
416
|
+
```json
|
|
417
|
+
{ "standards_overlay": "security/standards-overlay.yaml" }
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
```yaml
|
|
421
|
+
version: 1
|
|
422
|
+
entries:
|
|
423
|
+
- scanner: semgrep
|
|
424
|
+
rule_id: python.lang.security.audit.my-rule
|
|
425
|
+
canonical_cwe: CWE-22
|
|
426
|
+
owasp_top10: A01
|
|
427
|
+
asvs_section: V12.3.1
|
|
428
|
+
nist_ssdf: PW.5.1
|
|
429
|
+
short_desc: Path traversal in our request handler.
|
|
430
|
+
fix_hint: Resolve the path and contain it under the upload root.
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
An overlay entry wins over the shipped table for the fields it sets, and an
|
|
434
|
+
adapter asserting its own CWE still wins over both.
|
|
435
|
+
|
|
436
|
+
**An overlay cannot set `severity`, `confidence` or `category`**, and a file
|
|
437
|
+
that tries is refused with the reason rather than ignored. Those three are the
|
|
438
|
+
scoring inputs: an overlay that set them could move the grade, which is the
|
|
439
|
+
hole [D32](docs/decisions.md) closed on the configuration side. An overlay says
|
|
440
|
+
*what weakness a rule describes*; it does not say *how much it counts*. Set
|
|
441
|
+
severity policy with `severity_overrides` in a config kept outside the audited
|
|
442
|
+
tree.
|
|
443
|
+
|
|
444
|
+
A declared overlay that cannot be read fails the run rather than being skipped,
|
|
445
|
+
for the same reason a malformed `.scignore.yaml` does.
|
|
446
|
+
|
|
409
447
|
## Baseline + incremental adoption
|
|
410
448
|
|
|
411
449
|
`secure-code-baseline.json` fingerprints every current finding. On the next run:
|
|
@@ -467,7 +505,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
467
505
|
## GitHub Action
|
|
468
506
|
|
|
469
507
|
```yaml
|
|
470
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
508
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.11
|
|
471
509
|
with:
|
|
472
510
|
config: secure-code-agent.json
|
|
473
511
|
fail-on-gate: true
|
|
@@ -53,7 +53,7 @@ dependencies = [
|
|
|
53
53
|
# server drives it by subprocess rather than reimplementing it — so nobody
|
|
54
54
|
# who only wants the gate pays for an MCP dependency.
|
|
55
55
|
mcp = [
|
|
56
|
-
"mcp>=1.0,<
|
|
56
|
+
"mcp>=1.0,<3",
|
|
57
57
|
]
|
|
58
58
|
|
|
59
59
|
# Scanners the default configuration lists in gates.require_scanners. Pinned
|
|
@@ -66,7 +66,7 @@ required-scanners = [
|
|
|
66
66
|
# rules, so a JavaScript repository can be scanned on a host with no Node.
|
|
67
67
|
# Pinned for the same reason as the rest: the floor is a claim about what
|
|
68
68
|
# ran, and an unpinned ruleset makes that claim unreproducible.
|
|
69
|
-
"njsscan==1.0.
|
|
69
|
+
"njsscan==1.0.1",
|
|
70
70
|
]
|
|
71
71
|
# The remaining Python-packaged scanners. Bounded rather than pinned because
|
|
72
72
|
# they are optional; the coverage gate does not require them by default.
|
|
@@ -87,7 +87,12 @@ dev = [
|
|
|
87
87
|
"secure-code-agent[required-scanners]",
|
|
88
88
|
"pytest>=8.0",
|
|
89
89
|
"pytest-cov>=4.0",
|
|
90
|
-
"ruff==0.16.
|
|
90
|
+
"ruff==0.16.9",
|
|
91
|
+
# Renders docs/html, the reading copy of every document in the repository.
|
|
92
|
+
# A dev dependency, not a runtime one: the audit never renders documentation,
|
|
93
|
+
# and tests/unit/test_rendered_docs.py is what keeps the committed copy
|
|
94
|
+
# current. See tools/render_docs.py.
|
|
95
|
+
"markdown>=3.5",
|
|
91
96
|
]
|
|
92
97
|
|
|
93
98
|
[project.scripts]
|
|
@@ -133,14 +138,41 @@ ignore = ["E501"] # let line-length be a soft guide; long log strings are fine
|
|
|
133
138
|
|
|
134
139
|
[tool.pytest.ini_options]
|
|
135
140
|
testpaths = ["tests"]
|
|
136
|
-
|
|
141
|
+
# Coverage is on by default, so the declared test command — plain `pytest`,
|
|
142
|
+
# which is what maintainability-agent runs — reports it and the
|
|
143
|
+
# `fail_under` below actually applies. Without this the suite was
|
|
144
|
+
# measurable only by whoever remembered the flags, and MA scored
|
|
145
|
+
# test_effectiveness as "not measurable" on every run.
|
|
146
|
+
#
|
|
147
|
+
# `xml` is not decoration: a consumer reads the *artifact*, never stdout.
|
|
148
|
+
# MA looks for `coverage.xml`, `coverage/lcov.info` or `lcov.info` and
|
|
149
|
+
# requires it to be newer than the moment before it ran the suite, so a
|
|
150
|
+
# repository cannot hand it a committed report and set its own
|
|
151
|
+
# test_effectiveness. With `term` alone the number was printed for humans
|
|
152
|
+
# and invisible to every tool, which is the same defect one layer down.
|
|
153
|
+
# `coverage.xml` is gitignored.
|
|
154
|
+
#
|
|
155
|
+
# A step that is not the one measuring passes `--no-cov`;
|
|
156
|
+
# tests/unit/test_one_coverage_floor.py requires every CI invocation to say
|
|
157
|
+
# which it is.
|
|
158
|
+
addopts = "-q --strict-markers --cov=secure_code_audit --cov-branch --cov-report=term --cov-report=xml"
|
|
159
|
+
# The suite tests this checkout, never an installed copy. Without it, bare
|
|
160
|
+
# `pytest` imports whichever secure_code_audit the interpreter can find, and
|
|
161
|
+
# an environment holding a plain install grades code nobody is editing —
|
|
162
|
+
# which is how maintainability-agent came to report a bug fixed in #59.
|
|
163
|
+
pythonpath = ["src"]
|
|
137
164
|
|
|
138
165
|
[tool.coverage.run]
|
|
139
166
|
source = ["secure_code_audit"]
|
|
140
167
|
branch = true
|
|
141
168
|
|
|
142
169
|
[tool.coverage.report]
|
|
143
|
-
|
|
170
|
+
# The house floor. Raised 85 -> 92 once the suite could actually clear it:
|
|
171
|
+
# measured 93.79% branch coverage after the test seat covered --verify-against
|
|
172
|
+
# (which had no test through main() at all), the four live-config keys, and the
|
|
173
|
+
# loader's fail-closed paths. Raising it while the suite sat at 90.42% would
|
|
174
|
+
# have bought a red build and no tests.
|
|
175
|
+
fail_under = 92
|
|
144
176
|
exclude_lines = [
|
|
145
177
|
"pragma: no cover",
|
|
146
178
|
"if __name__ == .__main__.:",
|
{secure_code_agent-0.12.10 → secure_code_agent-0.12.11/src/secure_code_agent.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secure-code-agent
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.11
|
|
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
|
|
@@ -30,11 +30,11 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
Requires-Dist: PyYAML<7,>=6.0.2
|
|
32
32
|
Provides-Extra: mcp
|
|
33
|
-
Requires-Dist: mcp<
|
|
33
|
+
Requires-Dist: mcp<3,>=1.0; extra == "mcp"
|
|
34
34
|
Provides-Extra: required-scanners
|
|
35
35
|
Requires-Dist: bandit==1.9.4; extra == "required-scanners"
|
|
36
36
|
Requires-Dist: pip-audit==2.10.1; extra == "required-scanners"
|
|
37
|
-
Requires-Dist: njsscan==1.0.
|
|
37
|
+
Requires-Dist: njsscan==1.0.1; extra == "required-scanners"
|
|
38
38
|
Provides-Extra: python-scanners
|
|
39
39
|
Requires-Dist: secure-code-agent[required-scanners]; extra == "python-scanners"
|
|
40
40
|
Requires-Dist: semgrep<2,>=1.172; extra == "python-scanners"
|
|
@@ -43,7 +43,8 @@ Provides-Extra: dev
|
|
|
43
43
|
Requires-Dist: secure-code-agent[required-scanners]; extra == "dev"
|
|
44
44
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
45
45
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
46
|
-
Requires-Dist: ruff==0.16.
|
|
46
|
+
Requires-Dist: ruff==0.16.9; extra == "dev"
|
|
47
|
+
Requires-Dist: markdown>=3.5; extra == "dev"
|
|
47
48
|
Dynamic: license-file
|
|
48
49
|
|
|
49
50
|
# secure-code-agent
|
|
@@ -454,6 +455,44 @@ for repositories whose `pyproject.toml` is their authoritative audit input.
|
|
|
454
455
|
|
|
455
456
|
Wildcard rule (`rule_id: "*"`) requires a `file` or `paths` scope — you cannot disable a rule globally.
|
|
456
457
|
|
|
458
|
+
## Extending the standards mapping
|
|
459
|
+
|
|
460
|
+
The shipped mapping table lives in `data/standards.yaml` and covers the rules
|
|
461
|
+
this project curates. Semgrep alone publishes thousands, so point
|
|
462
|
+
`standards_overlay` at your own file to map or correct a rule without waiting
|
|
463
|
+
for a release:
|
|
464
|
+
|
|
465
|
+
```json
|
|
466
|
+
{ "standards_overlay": "security/standards-overlay.yaml" }
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
```yaml
|
|
470
|
+
version: 1
|
|
471
|
+
entries:
|
|
472
|
+
- scanner: semgrep
|
|
473
|
+
rule_id: python.lang.security.audit.my-rule
|
|
474
|
+
canonical_cwe: CWE-22
|
|
475
|
+
owasp_top10: A01
|
|
476
|
+
asvs_section: V12.3.1
|
|
477
|
+
nist_ssdf: PW.5.1
|
|
478
|
+
short_desc: Path traversal in our request handler.
|
|
479
|
+
fix_hint: Resolve the path and contain it under the upload root.
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
An overlay entry wins over the shipped table for the fields it sets, and an
|
|
483
|
+
adapter asserting its own CWE still wins over both.
|
|
484
|
+
|
|
485
|
+
**An overlay cannot set `severity`, `confidence` or `category`**, and a file
|
|
486
|
+
that tries is refused with the reason rather than ignored. Those three are the
|
|
487
|
+
scoring inputs: an overlay that set them could move the grade, which is the
|
|
488
|
+
hole [D32](docs/decisions.md) closed on the configuration side. An overlay says
|
|
489
|
+
*what weakness a rule describes*; it does not say *how much it counts*. Set
|
|
490
|
+
severity policy with `severity_overrides` in a config kept outside the audited
|
|
491
|
+
tree.
|
|
492
|
+
|
|
493
|
+
A declared overlay that cannot be read fails the run rather than being skipped,
|
|
494
|
+
for the same reason a malformed `.scignore.yaml` does.
|
|
495
|
+
|
|
457
496
|
## Baseline + incremental adoption
|
|
458
497
|
|
|
459
498
|
`secure-code-baseline.json` fingerprints every current finding. On the next run:
|
|
@@ -515,7 +554,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
515
554
|
## GitHub Action
|
|
516
555
|
|
|
517
556
|
```yaml
|
|
518
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
557
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.11
|
|
519
558
|
with:
|
|
520
559
|
config: secure-code-agent.json
|
|
521
560
|
fail-on-gate: true
|
{secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
@@ -31,7 +31,11 @@ src/secure_code_audit/suppressions.py
|
|
|
31
31
|
src/secure_code_audit/triage.py
|
|
32
32
|
src/secure_code_audit/verify.py
|
|
33
33
|
src/secure_code_audit/data/semgrep-offline.yaml
|
|
34
|
+
src/secure_code_audit/data/standards.yaml
|
|
34
35
|
src/secure_code_audit/scanners/__init__.py
|
|
36
|
+
src/secure_code_audit/scanners/_execution.py
|
|
37
|
+
src/secure_code_audit/scanners/_finding_builder.py
|
|
38
|
+
src/secure_code_audit/scanners/_resolution.py
|
|
35
39
|
src/secure_code_audit/scanners/bandit_scanner.py
|
|
36
40
|
src/secure_code_audit/scanners/base.py
|
|
37
41
|
src/secure_code_audit/scanners/builtin_rules.py
|
{secure_code_agent-0.12.10 → secure_code_agent-0.12.11}/src/secure_code_agent.egg-info/requires.txt
RENAMED
|
@@ -4,10 +4,11 @@ PyYAML<7,>=6.0.2
|
|
|
4
4
|
secure-code-agent[required-scanners]
|
|
5
5
|
pytest>=8.0
|
|
6
6
|
pytest-cov>=4.0
|
|
7
|
-
ruff==0.16.
|
|
7
|
+
ruff==0.16.9
|
|
8
|
+
markdown>=3.5
|
|
8
9
|
|
|
9
10
|
[mcp]
|
|
10
|
-
mcp<
|
|
11
|
+
mcp<3,>=1.0
|
|
11
12
|
|
|
12
13
|
[python-scanners]
|
|
13
14
|
secure-code-agent[required-scanners]
|
|
@@ -17,4 +18,4 @@ checkov<4,>=3.3
|
|
|
17
18
|
[required-scanners]
|
|
18
19
|
bandit==1.9.4
|
|
19
20
|
pip-audit==2.10.1
|
|
20
|
-
njsscan==1.0.
|
|
21
|
+
njsscan==1.0.1
|
|
@@ -21,6 +21,7 @@ from secure_code_audit import (
|
|
|
21
21
|
renderers,
|
|
22
22
|
sarif,
|
|
23
23
|
scanners,
|
|
24
|
+
standards,
|
|
24
25
|
suppressions,
|
|
25
26
|
triage,
|
|
26
27
|
)
|
|
@@ -361,87 +362,43 @@ def _print_preflight(rows: list[dict], unselected: list[str], blocking: list[str
|
|
|
361
362
|
print(" all required scanners resolved")
|
|
362
363
|
|
|
363
364
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
# Before anything is resolved or written, and before any scanner runs.
|
|
368
|
-
_assert_config_writes_are_contained(cfg, root, target)
|
|
369
|
-
# Resolved before the scan so the run can recognise its own artifacts.
|
|
370
|
-
paths = _resolve_outputs(args, cfg, root)
|
|
371
|
-
|
|
372
|
-
# ----- scanners -----
|
|
373
|
-
scan = _run_scanners(args, cfg, target)
|
|
374
|
-
all_findings = scan.findings
|
|
375
|
-
ran, unavailable, executions = scan.ran, scan.unavailable, scan.executions
|
|
376
|
-
|
|
377
|
-
# ----- SARIF imports -----
|
|
378
|
-
# An imported SARIF is coverage: a scanner someone else ran on our behalf.
|
|
379
|
-
imported, imported_executions = _ingest_sarif_imports(args.sarif_import)
|
|
380
|
-
all_findings.extend(imported)
|
|
381
|
-
executions.extend(imported_executions)
|
|
382
|
-
|
|
383
|
-
# ----- one path convention, before anything reads a path -----
|
|
384
|
-
# Repository-relative from here on, whichever tool reported the finding.
|
|
385
|
-
# Suppressions, the baseline and every written output compare or publish
|
|
386
|
-
# this path, and none of them may depend on where the checkout lives.
|
|
387
|
-
scanned = target if target.is_dir() else target.parent
|
|
388
|
-
all_findings = anchor(all_findings, scanned=scanned, root=root)
|
|
389
|
-
|
|
390
|
-
# ----- scan scope, enforced once -----
|
|
391
|
-
own_artifacts = _own_artifacts(paths, cfg, root)
|
|
392
|
-
all_findings = _drop_excluded(all_findings, target, cfg, own_artifacts, root)
|
|
393
|
-
|
|
394
|
-
# ----- one weakness, one finding -----
|
|
395
|
-
# Before overrides and suppressions, so an operator writing either one
|
|
396
|
-
# sees the same finding the report will show. Bandit's B308 and B703 are
|
|
397
|
-
# the same check under two ids and shared fifty lines in Django; a report
|
|
398
|
-
# that lists a line twice is wrong about the code, and a work order built
|
|
399
|
-
# from it would ask for the same fix twice.
|
|
400
|
-
all_findings = merge_corroborating(all_findings)
|
|
401
|
-
|
|
402
|
-
# ----- overrides from config -----
|
|
403
|
-
all_findings = _apply_overrides(all_findings, cfg)
|
|
404
|
-
|
|
405
|
-
# ----- suppressions -----
|
|
406
|
-
suppression_path = _under_root(root, cfg.suppressions_file)
|
|
407
|
-
sup_rules, sup_errors = suppressions.load(suppression_path)
|
|
408
|
-
if sup_errors:
|
|
409
|
-
# Fail closed. A suppression file that exists is an explicit
|
|
410
|
-
# instruction; ignoring it silently changes which findings are
|
|
411
|
-
# reported, and "my suppressions are working" then looks exactly
|
|
412
|
-
# like "my suppressions were skipped". This bit in practice: a
|
|
413
|
-
# missing PyYAML made an entire .scignore.yaml a no-op while the
|
|
414
|
-
# run still exited 0 and reported the suppressed finding as live.
|
|
415
|
-
#
|
|
416
|
-
# Only reachable when the file is present, so repositories that
|
|
417
|
-
# do not use suppressions are unaffected.
|
|
418
|
-
for err in sup_errors:
|
|
419
|
-
sys.stderr.write(f"ERROR: {err}\n")
|
|
420
|
-
sys.stderr.write(
|
|
421
|
-
f"ERROR: {suppression_path} exists but could not be applied; refusing to "
|
|
422
|
-
"report results that silently ignore it.\n"
|
|
423
|
-
)
|
|
424
|
-
return 1
|
|
425
|
-
all_findings = suppressions.apply(all_findings, sup_rules, root)
|
|
426
|
-
all_findings.extend(
|
|
427
|
-
anchor(
|
|
428
|
-
suppressions.expired_findings(sup_rules, suppression_path),
|
|
429
|
-
scanned=scanned,
|
|
430
|
-
root=root,
|
|
431
|
-
)
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
# ----- severity threshold filter -----
|
|
435
|
-
threshold = Severity.from_string(args.severity_threshold)
|
|
436
|
-
all_findings = [f for f in all_findings if f.severity.rank >= threshold.rank]
|
|
365
|
+
@dataclass(frozen=True)
|
|
366
|
+
class _Axes:
|
|
367
|
+
"""What the axis split produced, and the LOC that goes with it.
|
|
437
368
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
baseline_state = baseline_mod.state(baseline_path)
|
|
442
|
-
all_findings = baseline_mod.mark_new(all_findings, baseline, root)
|
|
369
|
+
Seven values travelled together as seven locals inside a 346-line
|
|
370
|
+
function; a dataclass is what they were already.
|
|
371
|
+
"""
|
|
443
372
|
|
|
444
|
-
|
|
373
|
+
primary: list[Finding]
|
|
374
|
+
test: list[Finding]
|
|
375
|
+
docs: list[Finding]
|
|
376
|
+
#: Kept whole as well as split: `declared_axes` reads it by name for the
|
|
377
|
+
#: capability axes, which are not a fixed set.
|
|
378
|
+
path_axes: dict[str, list[Finding]]
|
|
379
|
+
all_findings: list[Finding]
|
|
380
|
+
loc: int
|
|
381
|
+
test_loc: int
|
|
382
|
+
docs_loc: int
|
|
383
|
+
changed_note: str | None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _split_axes(
|
|
387
|
+
all_findings: list[Finding],
|
|
388
|
+
args: argparse.Namespace,
|
|
389
|
+
cfg: config_mod.Config,
|
|
390
|
+
target: Path,
|
|
391
|
+
root: Path,
|
|
392
|
+
own_artifacts: frozenset[Path],
|
|
393
|
+
) -> _Axes:
|
|
394
|
+
"""Partition findings by axis and measure each axis's LOC.
|
|
395
|
+
|
|
396
|
+
Lifted whole out of `_do_audit`, which was 346 lines at complexity 36
|
|
397
|
+
against configured limits of 80 and 15. The checks, their order and
|
|
398
|
+
their comments are unchanged: this is a move, not a rewrite, because a
|
|
399
|
+
rewrite of the axis split is how a repository gets graded on its test
|
|
400
|
+
fixtures and the corpus has already shown what that costs.
|
|
401
|
+
"""
|
|
445
402
|
# The test tree is reported, not scored. A project graded on its test
|
|
446
403
|
# fixtures is graded on the wrong thing: across the calibration corpus,
|
|
447
404
|
# including test directories moved the median normalized subtotal from
|
|
@@ -522,6 +479,124 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
522
479
|
own_artifacts,
|
|
523
480
|
cfg.docs_patterns,
|
|
524
481
|
)
|
|
482
|
+
|
|
483
|
+
return _Axes(
|
|
484
|
+
primary=primary_findings,
|
|
485
|
+
test=test_findings,
|
|
486
|
+
docs=docs_findings,
|
|
487
|
+
path_axes=path_axes,
|
|
488
|
+
all_findings=all_findings,
|
|
489
|
+
loc=loc,
|
|
490
|
+
test_loc=test_loc,
|
|
491
|
+
docs_loc=docs_loc,
|
|
492
|
+
changed_note=changed_note,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _do_audit(args: argparse.Namespace) -> int:
|
|
497
|
+
cfg, target, root = _prepare_audit(args)
|
|
498
|
+
# Before any consumer reads them: the audited tree does not grade itself.
|
|
499
|
+
cfg = _refuse_target_policy(cfg, target)
|
|
500
|
+
_require_configured_gates(args, cfg)
|
|
501
|
+
if (failed := _install_standards_overlay(cfg, root)) is not None:
|
|
502
|
+
return failed
|
|
503
|
+
# Before anything is resolved or written, and before any scanner runs.
|
|
504
|
+
_assert_config_writes_are_contained(cfg, root, target)
|
|
505
|
+
# Resolved before the scan so the run can recognise its own artifacts.
|
|
506
|
+
paths = _resolve_outputs(args, cfg, root)
|
|
507
|
+
|
|
508
|
+
# ----- scanners -----
|
|
509
|
+
scan = _run_scanners(args, cfg, target)
|
|
510
|
+
all_findings = scan.findings
|
|
511
|
+
ran, unavailable, executions = scan.ran, scan.unavailable, scan.executions
|
|
512
|
+
|
|
513
|
+
# ----- SARIF imports -----
|
|
514
|
+
# An imported SARIF is coverage: a scanner someone else ran on our behalf.
|
|
515
|
+
imported, imported_executions = _ingest_sarif_imports(args.sarif_import)
|
|
516
|
+
all_findings.extend(imported)
|
|
517
|
+
executions.extend(imported_executions)
|
|
518
|
+
|
|
519
|
+
# ----- one path convention, before anything reads a path -----
|
|
520
|
+
# Repository-relative from here on, whichever tool reported the finding.
|
|
521
|
+
# Suppressions, the baseline and every written output compare or publish
|
|
522
|
+
# this path, and none of them may depend on where the checkout lives.
|
|
523
|
+
scanned = target if target.is_dir() else target.parent
|
|
524
|
+
all_findings = anchor(all_findings, scanned=scanned, root=root)
|
|
525
|
+
|
|
526
|
+
# ----- scan scope, enforced once -----
|
|
527
|
+
own_artifacts = _own_artifacts(paths, cfg, root)
|
|
528
|
+
all_findings = _drop_excluded(all_findings, target, cfg, own_artifacts, root)
|
|
529
|
+
|
|
530
|
+
# ----- one weakness, one finding -----
|
|
531
|
+
# Before overrides and suppressions, so an operator writing either one
|
|
532
|
+
# sees the same finding the report will show. Bandit's B308 and B703 are
|
|
533
|
+
# the same check under two ids and shared fifty lines in Django; a report
|
|
534
|
+
# that lists a line twice is wrong about the code, and a work order built
|
|
535
|
+
# from it would ask for the same fix twice.
|
|
536
|
+
all_findings = merge_corroborating(all_findings)
|
|
537
|
+
|
|
538
|
+
# ----- overrides from config -----
|
|
539
|
+
all_findings = _apply_overrides(all_findings, cfg)
|
|
540
|
+
|
|
541
|
+
# ----- suppressions -----
|
|
542
|
+
suppression_path = _under_root(root, cfg.suppressions_file)
|
|
543
|
+
sup_rules, sup_errors = suppressions.load(suppression_path)
|
|
544
|
+
if sup_errors:
|
|
545
|
+
# Fail closed. A suppression file that exists is an explicit
|
|
546
|
+
# instruction; ignoring it silently changes which findings are
|
|
547
|
+
# reported, and "my suppressions are working" then looks exactly
|
|
548
|
+
# like "my suppressions were skipped". This bit in practice: a
|
|
549
|
+
# missing PyYAML made an entire .scignore.yaml a no-op while the
|
|
550
|
+
# run still exited 0 and reported the suppressed finding as live.
|
|
551
|
+
#
|
|
552
|
+
# Only reachable when the file is present, so repositories that
|
|
553
|
+
# do not use suppressions are unaffected.
|
|
554
|
+
for err in sup_errors:
|
|
555
|
+
sys.stderr.write(f"ERROR: {err}\n")
|
|
556
|
+
sys.stderr.write(
|
|
557
|
+
f"ERROR: {suppression_path} exists but could not be applied; refusing to "
|
|
558
|
+
"report results that silently ignore it.\n"
|
|
559
|
+
)
|
|
560
|
+
return 1
|
|
561
|
+
all_findings = suppressions.apply(all_findings, sup_rules, root)
|
|
562
|
+
# Snapshot the scanner findings before the synthetic ones are appended.
|
|
563
|
+
# `unused_findings` asks whether each rule still has a subject, and a
|
|
564
|
+
# wildcard rule with `paths` would happily match the expired-suppression
|
|
565
|
+
# finding about `.scignore.yaml` itself — reporting itself as used
|
|
566
|
+
# because it matched a finding about suppressions.
|
|
567
|
+
scanner_findings = list(all_findings)
|
|
568
|
+
all_findings.extend(
|
|
569
|
+
anchor(
|
|
570
|
+
suppressions.expired_findings(sup_rules, suppression_path),
|
|
571
|
+
scanned=scanned,
|
|
572
|
+
root=root,
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
all_findings.extend(
|
|
576
|
+
anchor(
|
|
577
|
+
suppressions.unused_findings(sup_rules, scanner_findings, suppression_path, root),
|
|
578
|
+
scanned=scanned,
|
|
579
|
+
root=root,
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# ----- severity threshold filter -----
|
|
584
|
+
threshold = Severity.from_string(args.severity_threshold)
|
|
585
|
+
all_findings = [f for f in all_findings if f.severity.rank >= threshold.rank]
|
|
586
|
+
|
|
587
|
+
# ----- baseline -----
|
|
588
|
+
baseline_path = _under_root(root, args.baseline or cfg.outputs["baseline_path"])
|
|
589
|
+
baseline = baseline_mod.load(baseline_path)
|
|
590
|
+
baseline_state = baseline_mod.state(baseline_path)
|
|
591
|
+
all_findings = baseline_mod.mark_new(all_findings, baseline, root)
|
|
592
|
+
|
|
593
|
+
# ----- scoring -----
|
|
594
|
+
_axes = _split_axes(all_findings, args, cfg, target, root, own_artifacts)
|
|
595
|
+
all_findings = _axes.all_findings
|
|
596
|
+
primary_findings, test_findings, docs_findings = _axes.primary, _axes.test, _axes.docs
|
|
597
|
+
path_axes = _axes.path_axes
|
|
598
|
+
loc, test_loc, docs_loc = _axes.loc, _axes.test_loc, _axes.docs_loc
|
|
599
|
+
changed_note = _axes.changed_note
|
|
525
600
|
# Dependencies come off the code-condition score and onto their own axis.
|
|
526
601
|
# A CVE in a pinned dependency is fixed with a version bump; an injection
|
|
527
602
|
# flaw is fixed with a rewrite. Averaging them produced the largest
|
|
@@ -1070,6 +1145,78 @@ def _write_outputs(
|
|
|
1070
1145
|
pillar_mod.write(pillar, paths.security_pillar)
|
|
1071
1146
|
|
|
1072
1147
|
|
|
1148
|
+
def _install_standards_overlay(cfg: config_mod.Config, root: Path) -> int | None:
|
|
1149
|
+
"""Layer the operator's standards mapping over the shipped table.
|
|
1150
|
+
|
|
1151
|
+
Returns an exit code when the overlay cannot be applied, else None.
|
|
1152
|
+
|
|
1153
|
+
Fails closed, for the same reason a malformed suppressions file does: the
|
|
1154
|
+
operator named a mapping file, so running without it reports findings
|
|
1155
|
+
under the standards nobody chose — unmapped where they should be mapped,
|
|
1156
|
+
and no louder than a rule nobody has curated. Reachable only when the key
|
|
1157
|
+
is set, so repositories that do not use an overlay are unaffected.
|
|
1158
|
+
"""
|
|
1159
|
+
if not cfg.standards_overlay:
|
|
1160
|
+
return None
|
|
1161
|
+
path = _under_root(root, cfg.standards_overlay)
|
|
1162
|
+
entries, errors = standards.load_overlay(path)
|
|
1163
|
+
if errors:
|
|
1164
|
+
for err in errors:
|
|
1165
|
+
sys.stderr.write(f"ERROR: {err}\n")
|
|
1166
|
+
sys.stderr.write(
|
|
1167
|
+
f"ERROR: standards_overlay {path} is declared but could not be applied; "
|
|
1168
|
+
"refusing to report findings mapped by a table the operator did not choose.\n"
|
|
1169
|
+
)
|
|
1170
|
+
return 1
|
|
1171
|
+
standards.install_overlay(entries)
|
|
1172
|
+
return None
|
|
1173
|
+
|
|
1174
|
+
|
|
1175
|
+
#: Config keys that move the grade, and so may not come from the audited tree.
|
|
1176
|
+
#: `severity_overrides` sets a finding's weight — `INFORMATIONAL` is 0.0 in
|
|
1177
|
+
#: `SEVERITY_WEIGHT`, so re-labelling removes a finding from the score while
|
|
1178
|
+
#: leaving it in the report. `category_overrides` moves it between the
|
|
1179
|
+
#: per-category rates, and the worst category drives the overall.
|
|
1180
|
+
_GRADING_KEYS = ("severity_overrides", "category_overrides")
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _refuse_target_policy(cfg: config_mod.Config, target: Path) -> config_mod.Config:
|
|
1184
|
+
"""Drop grading keys supplied by the audited tree. Returns the config to use.
|
|
1185
|
+
|
|
1186
|
+
D29 settled that a config the tree supplies does not choose a scanner's
|
|
1187
|
+
command. It said nothing about what that config may claim about the
|
|
1188
|
+
findings, and these two keys were applied from any config, including one
|
|
1189
|
+
discovered inside the tree, with no `--trust-target-config`.
|
|
1190
|
+
|
|
1191
|
+
Measured on one `subprocess` call made with `shell=True` — paraphrased
|
|
1192
|
+
rather than quoted, because this repository audits its own source and
|
|
1193
|
+
the verbatim expression trips `sca.python.subprocess.shell_true` here,
|
|
1194
|
+
in shipped code, where it is scored: 0.00/F honestly, and
|
|
1195
|
+
5.00/A+ with an in-tree `severity_overrides` mapping B602 to
|
|
1196
|
+
informational. The audited party moved its own grade, which is §4
|
|
1197
|
+
criterion 1 falsifiable by the one actor with a motive, and promise P3
|
|
1198
|
+
from the other side — P3 refuses an input whose *removal* raises the
|
|
1199
|
+
graded field, and this is one whose *addition* raises it.
|
|
1200
|
+
|
|
1201
|
+
Not silent, and not fatal. A repository may carry this config for its
|
|
1202
|
+
own operator's use, and that operator says so with
|
|
1203
|
+
`--trust-target-config` or by keeping the config outside the tree —
|
|
1204
|
+
exactly the two routes that already work for a command.
|
|
1205
|
+
"""
|
|
1206
|
+
if not config_mod.target_config_is_untrusted(cfg, target):
|
|
1207
|
+
return cfg
|
|
1208
|
+
ignored = [key for key in _GRADING_KEYS if getattr(cfg, key)]
|
|
1209
|
+
if not ignored:
|
|
1210
|
+
return cfg
|
|
1211
|
+
for key in ignored:
|
|
1212
|
+
sys.stderr.write(
|
|
1213
|
+
f"WARNING: {key} in {cfg.source_path} is inside the audited tree and "
|
|
1214
|
+
f"changes the grade, so it was ignored. Pass --trust-target-config, or "
|
|
1215
|
+
f"keep the configuration outside the tree, to apply it.\n"
|
|
1216
|
+
)
|
|
1217
|
+
return replace(cfg, severity_overrides={}, category_overrides={})
|
|
1218
|
+
|
|
1219
|
+
|
|
1073
1220
|
def _apply_overrides(findings: list[Finding], cfg: config_mod.Config) -> list[Finding]:
|
|
1074
1221
|
"""Apply config-level severity_overrides + category_overrides."""
|
|
1075
1222
|
if not cfg.severity_overrides and not cfg.category_overrides:
|