secure-code-agent 0.12.12__tar.gz → 0.13.1__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.12/src/secure_code_agent.egg-info → secure_code_agent-0.13.1}/PKG-INFO +4 -2
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/README.md +3 -1
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1/src/secure_code_agent.egg-info}/PKG-INFO +4 -2
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/__init__.py +1 -1
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/cli.py +29 -2
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/git_tools.py +41 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/renderers.py +13 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/sarif.py +35 -11
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanner_status.py +124 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scoring.py +24 -3
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/LICENSE +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/pyproject.toml +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/setup.cfg +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/SOURCES.txt +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/requires.txt +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/baseline.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/capabilities.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/config.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/data/standards.yaml +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/demo.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/findings.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/history.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/mcp_server.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/pillar.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/practice.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/remediation.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_exclusions.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_execution.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_finding_builder.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_resolution.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/base.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/standards.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/suppressions.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/triage.py +0 -0
- {secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/verify.py +0 -0
{secure_code_agent-0.12.12/src/secure_code_agent.egg-info → secure_code_agent-0.13.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secure-code-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
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
|
|
@@ -49,6 +49,8 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# secure-code-agent
|
|
51
51
|
|
|
52
|
+

|
|
53
|
+
|
|
52
54
|
> **Deterministic security gate + bounded AI remediation prompts for repos with AI coding agents in the loop.**
|
|
53
55
|
> Anchored to NIST SSDF · OWASP ASVS · OWASP Top 10 · MITRE CWE Top 25 · OpenSSF Scorecard · SARIF 2.1.0.
|
|
54
56
|
|
|
@@ -554,7 +556,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
554
556
|
## GitHub Action
|
|
555
557
|
|
|
556
558
|
```yaml
|
|
557
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
559
|
+
- uses: marshallguillory86/secure-code-agent@v0.13.1
|
|
558
560
|
with:
|
|
559
561
|
config: secure-code-agent.json
|
|
560
562
|
fail-on-gate: true
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# secure-code-agent
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
> **Deterministic security gate + bounded AI remediation prompts for repos with AI coding agents in the loop.**
|
|
4
6
|
> Anchored to NIST SSDF · OWASP ASVS · OWASP Top 10 · MITRE CWE Top 25 · OpenSSF Scorecard · SARIF 2.1.0.
|
|
5
7
|
|
|
@@ -505,7 +507,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
505
507
|
## GitHub Action
|
|
506
508
|
|
|
507
509
|
```yaml
|
|
508
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
510
|
+
- uses: marshallguillory86/secure-code-agent@v0.13.1
|
|
509
511
|
with:
|
|
510
512
|
config: secure-code-agent.json
|
|
511
513
|
fail-on-gate: true
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1/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.
|
|
3
|
+
Version: 0.13.1
|
|
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
|
|
@@ -49,6 +49,8 @@ Dynamic: license-file
|
|
|
49
49
|
|
|
50
50
|
# secure-code-agent
|
|
51
51
|
|
|
52
|
+

|
|
53
|
+
|
|
52
54
|
> **Deterministic security gate + bounded AI remediation prompts for repos with AI coding agents in the loop.**
|
|
53
55
|
> Anchored to NIST SSDF · OWASP ASVS · OWASP Top 10 · MITRE CWE Top 25 · OpenSSF Scorecard · SARIF 2.1.0.
|
|
54
56
|
|
|
@@ -554,7 +556,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
554
556
|
## GitHub Action
|
|
555
557
|
|
|
556
558
|
```yaml
|
|
557
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
559
|
+
- uses: marshallguillory86/secure-code-agent@v0.13.1
|
|
558
560
|
with:
|
|
559
561
|
config: secure-code-agent.json
|
|
560
562
|
fail-on-gate: true
|
|
@@ -44,13 +44,20 @@ from secure_code_audit.findings import (
|
|
|
44
44
|
anchor,
|
|
45
45
|
merge_corroborating,
|
|
46
46
|
)
|
|
47
|
-
from secure_code_audit.git_tools import
|
|
47
|
+
from secure_code_audit.git_tools import (
|
|
48
|
+
extensions_under,
|
|
49
|
+
find_repo_root,
|
|
50
|
+
is_excluded,
|
|
51
|
+
is_test_path,
|
|
52
|
+
loc_under,
|
|
53
|
+
)
|
|
48
54
|
from secure_code_audit.scanner_status import (
|
|
49
55
|
COVERING_OUTCOMES,
|
|
50
56
|
ScannerExecution,
|
|
51
57
|
ScannerOutcome,
|
|
52
58
|
evaluate_coverage,
|
|
53
59
|
execution_from_result,
|
|
60
|
+
languages_for,
|
|
54
61
|
)
|
|
55
62
|
from secure_code_audit.scanners import floor
|
|
56
63
|
from secure_code_audit.scoring import (
|
|
@@ -622,7 +629,27 @@ def _score_everything(
|
|
|
622
629
|
),
|
|
623
630
|
*(execution.name for execution in imported_executions),
|
|
624
631
|
]
|
|
625
|
-
|
|
632
|
+
# The languages the *scored* tree actually contains, so COMPLETE can mean
|
|
633
|
+
# "this tree was covered" rather than "every configured scanner ran".
|
|
634
|
+
# `gin` — 7,146 lines of Go — reported COMPLETE and `code_vulnerabilities:
|
|
635
|
+
# 5.0` while bandit, njsscan and rubocop found nothing in Go source and
|
|
636
|
+
# gosec was not in the run. P7's own falsification condition.
|
|
637
|
+
#
|
|
638
|
+
# Scoped exactly as the scoring denominator is, which is why it is
|
|
639
|
+
# `extensions_under` and not `_repository_inventory`: the broad inventory
|
|
640
|
+
# answers "is there any Go here at all" for applicability, and a Go fixture
|
|
641
|
+
# under `tests/` must not oblige a Python project to install gosec.
|
|
642
|
+
primary_languages = languages_for(
|
|
643
|
+
extensions_under(
|
|
644
|
+
target,
|
|
645
|
+
cfg.include_extensions,
|
|
646
|
+
cfg.exclude_patterns,
|
|
647
|
+
cfg.test_patterns,
|
|
648
|
+
own_artifacts,
|
|
649
|
+
cfg.docs_patterns,
|
|
650
|
+
)
|
|
651
|
+
)
|
|
652
|
+
coverage = evaluate_coverage(executions, required, primary_languages)
|
|
626
653
|
# Gates see the dependency advisories; the score does not.
|
|
627
654
|
# The gate needs to know *why* the baseline is empty to describe a first
|
|
628
655
|
# run truthfully. Passed in the config dict rather than as a parameter so
|
|
@@ -229,6 +229,47 @@ def loc_under(
|
|
|
229
229
|
return primary, test, docs
|
|
230
230
|
|
|
231
231
|
|
|
232
|
+
def extensions_under(
|
|
233
|
+
root: Path,
|
|
234
|
+
include_exts: Iterable[str],
|
|
235
|
+
excludes: Iterable[str],
|
|
236
|
+
test_patterns: Iterable[str] = (),
|
|
237
|
+
skip: Iterable[Path] = (),
|
|
238
|
+
docs_patterns: Iterable[str] = (),
|
|
239
|
+
) -> tuple[str, ...]:
|
|
240
|
+
"""The file extensions of the **primary** tree, sorted.
|
|
241
|
+
|
|
242
|
+
The input to coverage's question "was this tree covered". Scoped exactly
|
|
243
|
+
as `loc_under`'s primary count is, and for the same reason: a claim about
|
|
244
|
+
coverage has to describe the tree the score describes.
|
|
245
|
+
|
|
246
|
+
Primary only. A Go fixture under `tests/` does not oblige a Python
|
|
247
|
+
project to install `gosec` — the test tree is reported and not scored, so
|
|
248
|
+
a language appearing only there cannot make the score unsupported.
|
|
249
|
+
"""
|
|
250
|
+
test_patterns = tuple(test_patterns)
|
|
251
|
+
docs_patterns = tuple(docs_patterns)
|
|
252
|
+
skip = {path.resolve() for path in skip}
|
|
253
|
+
found: set[str] = set()
|
|
254
|
+
# `rglob` on a file yields nothing, the same case `loc_under` documents.
|
|
255
|
+
candidates = root.rglob("*") if root.is_dir() else [root]
|
|
256
|
+
for path in candidates:
|
|
257
|
+
if not path.is_file():
|
|
258
|
+
continue
|
|
259
|
+
if path.resolve() in skip:
|
|
260
|
+
continue
|
|
261
|
+
if is_excluded(path, root, excludes):
|
|
262
|
+
continue
|
|
263
|
+
if not in_scope(path, include_exts):
|
|
264
|
+
continue
|
|
265
|
+
if test_patterns and is_test_path(path, root, test_patterns):
|
|
266
|
+
continue
|
|
267
|
+
if docs_patterns and is_test_path(path, root, docs_patterns):
|
|
268
|
+
continue
|
|
269
|
+
found.add(path.suffix)
|
|
270
|
+
return tuple(sorted(found))
|
|
271
|
+
|
|
272
|
+
|
|
232
273
|
def _git() -> str | None:
|
|
233
274
|
"""Resolve `git` to an absolute path, once.
|
|
234
275
|
|
|
@@ -137,6 +137,10 @@ def _coverage_to_dict(coverage: CoverageReport | None) -> dict | None:
|
|
|
137
137
|
"required": list(coverage.required),
|
|
138
138
|
"unverified": list(coverage.unverified),
|
|
139
139
|
"failures": list(coverage.failures),
|
|
140
|
+
# Languages the scored tree contains that nothing in the run reads.
|
|
141
|
+
# Carried here because MA's security pillar and any other machine
|
|
142
|
+
# consumer needs to know the grade's breadth, not just its value.
|
|
143
|
+
"unread_languages": list(coverage.unread_languages),
|
|
140
144
|
"scanners": [
|
|
141
145
|
{
|
|
142
146
|
"name": execution.name,
|
|
@@ -305,6 +309,13 @@ def _summary_section(
|
|
|
305
309
|
out = ["## Summary", ""]
|
|
306
310
|
if coverage is not None:
|
|
307
311
|
out.append(f"- **Scanner coverage:** {coverage.status.value.upper()}")
|
|
312
|
+
# The language *and* the remedy, in the summary rather than only in
|
|
313
|
+
# the scanners section. "scanner coverage is partial" tells a reader
|
|
314
|
+
# that something is missing, not what — the same half-answer that
|
|
315
|
+
# said `did not complete: failed` while trivy's 429 and its fix sat
|
|
316
|
+
# one field away in the same record.
|
|
317
|
+
for entry in coverage.unread_languages:
|
|
318
|
+
out.append(f"- **Language not covered:** {entry}")
|
|
308
319
|
# No verdict means nobody decided this run could claim a grade, so it
|
|
309
320
|
# cannot. Falling back to a locally-invented condition here is what put a
|
|
310
321
|
# second decision rule in the codebase in the first place.
|
|
@@ -421,6 +432,8 @@ def _scanners_section(
|
|
|
421
432
|
out.append(f"- Coverage: **{coverage.status.value.upper()}**")
|
|
422
433
|
if coverage.required:
|
|
423
434
|
out.append(f"- Required: {', '.join(coverage.required)}")
|
|
435
|
+
for entry in coverage.unread_languages:
|
|
436
|
+
out.append(f"- Not read by any scanner that ran: {entry}")
|
|
424
437
|
if coverage.unverified:
|
|
425
438
|
out.append(
|
|
426
439
|
f"- Unverified (imported artifact, execution not observed): "
|
|
@@ -13,8 +13,8 @@ from pathlib import Path
|
|
|
13
13
|
from secure_code_audit import __version__
|
|
14
14
|
from secure_code_audit.findings import Category, Confidence, Finding, Severity
|
|
15
15
|
from secure_code_audit.scanner_status import (
|
|
16
|
+
COVERING_OUTCOMES,
|
|
16
17
|
CoverageReport,
|
|
17
|
-
CoverageStatus,
|
|
18
18
|
ScannerExecution,
|
|
19
19
|
ScannerOutcome,
|
|
20
20
|
)
|
|
@@ -92,8 +92,20 @@ def emit(
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def _invocation(coverage: CoverageReport) -> dict:
|
|
95
|
+
"""The run, and what it covered, on two separate axes.
|
|
96
|
+
|
|
97
|
+
`executionSuccessful` says whether this tool ran to completion, and a
|
|
98
|
+
document is only written when it did. It was set from coverage instead,
|
|
99
|
+
so `maintainability-agent` — every scanner completed, PARTIAL only because
|
|
100
|
+
nothing in the floor reads shell — uploaded "unsuccessful tool execution,
|
|
101
|
+
exit code 0" on every run, and GitHub code scanning put a configuration
|
|
102
|
+
error banner on its Security overview. Coverage stays in
|
|
103
|
+
`properties.coverageStatus`, and each gap is a notification naming what
|
|
104
|
+
was not read: `error` for a required scanner that broke the gate,
|
|
105
|
+
`warning` for a limit of the floor.
|
|
106
|
+
"""
|
|
95
107
|
invocation = {
|
|
96
|
-
"executionSuccessful":
|
|
108
|
+
"executionSuccessful": True,
|
|
97
109
|
"properties": {
|
|
98
110
|
"coverageStatus": coverage.status.value,
|
|
99
111
|
"requiredScanners": list(coverage.required),
|
|
@@ -108,18 +120,30 @@ def _invocation(coverage: CoverageReport) -> dict:
|
|
|
108
120
|
],
|
|
109
121
|
},
|
|
110
122
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
notifications = [_coverage_notification("error", failure) for failure in coverage.failures]
|
|
124
|
+
notifications += [
|
|
125
|
+
_coverage_notification("warning", f"language not covered: {entry}")
|
|
126
|
+
for entry in coverage.unread_languages
|
|
127
|
+
]
|
|
128
|
+
notifications += [
|
|
129
|
+
_coverage_notification(
|
|
130
|
+
"warning",
|
|
131
|
+
f"scanner {execution.name!r} did not complete: {execution.outcome.value}"
|
|
132
|
+
+ (f" — {execution.reason}" if execution.reason else ""),
|
|
133
|
+
)
|
|
134
|
+
for execution in coverage.executions
|
|
135
|
+
if execution.name not in coverage.required
|
|
136
|
+
and execution.outcome not in (*COVERING_OUTCOMES, ScannerOutcome.NOT_APPLICABLE)
|
|
137
|
+
]
|
|
138
|
+
if notifications:
|
|
139
|
+
invocation["toolExecutionNotifications"] = notifications
|
|
120
140
|
return invocation
|
|
121
141
|
|
|
122
142
|
|
|
143
|
+
def _coverage_notification(level: str, text: str) -> dict:
|
|
144
|
+
return {"level": level, "message": {"text": text}, "descriptor": {"id": "scanner-coverage"}}
|
|
145
|
+
|
|
146
|
+
|
|
123
147
|
def _rule(f: Finding) -> dict:
|
|
124
148
|
rule: dict = {
|
|
125
149
|
"id": f.rule_id,
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanner_status.py
RENAMED
|
@@ -91,6 +91,12 @@ class CoverageReport:
|
|
|
91
91
|
#: than a process we watched. Reported everywhere so COMPLETE is never
|
|
92
92
|
#: read as "we verified all of this ourselves".
|
|
93
93
|
unverified: tuple[str, ...] = ()
|
|
94
|
+
#: Languages present in the tree that nothing in this run reads for code
|
|
95
|
+
#: vulnerabilities. Separate from `failures` on purpose: a required
|
|
96
|
+
#: scanner that did not run broke a promise the operator made and fails
|
|
97
|
+
#: the gate, while an unread language is a limit of the floor and
|
|
98
|
+
#: withholds the verified grade instead.
|
|
99
|
+
unread_languages: tuple[str, ...] = ()
|
|
94
100
|
|
|
95
101
|
|
|
96
102
|
def execution_from_result(
|
|
@@ -150,10 +156,115 @@ def worst_by_name(
|
|
|
150
156
|
return by_name
|
|
151
157
|
|
|
152
158
|
|
|
159
|
+
#: Which scanners in this floor actually read a language for *code* defects.
|
|
160
|
+
#:
|
|
161
|
+
#: **Verified by running them, not adopted from anywhere.** An earlier version
|
|
162
|
+
#: of this lifted `calibration/calibrate.py`'s `LANGUAGE_SCANNERS` wholesale
|
|
163
|
+
#: and was wrong, because that map answers a different question: the study
|
|
164
|
+
#: fixes its scanner set and uses "did the *dedicated* language scanner run"
|
|
165
|
+
#: as a proxy for coverage depth. Read as "what reads this language at all" it
|
|
166
|
+
#: asserts things that are false — that nothing reads Go without `gosec`
|
|
167
|
+
#: (semgrep's offline profile carries five Go rules, and they fire), and that
|
|
168
|
+
#: nothing reads Python without `bandit` (`builtin_rules` is Python-only and
|
|
169
|
+
#: found the findings in the very fixture that caught this).
|
|
170
|
+
#:
|
|
171
|
+
#: So this lists every scanner that genuinely reads the language, and a
|
|
172
|
+
#: language is flagged only when **nothing** read it. That is a weaker claim
|
|
173
|
+
#: than the study's and it is the one that is true.
|
|
174
|
+
#:
|
|
175
|
+
#: The depth question the study is really asking — "five generic rules is a
|
|
176
|
+
#: floor, not coverage" — is deliberately *not* answered here. `COMPLETE` and
|
|
177
|
+
#: `PARTIAL` cannot express it, and inventing a third state is a product
|
|
178
|
+
#: decision rather than a bug fix.
|
|
179
|
+
LANGUAGE_SCANNERS: dict[str, tuple[str, ...]] = {
|
|
180
|
+
# `builtin_rules` is Python-only; semgrep's offline profile adds two more.
|
|
181
|
+
"python": ("bandit", "semgrep", "builtin_rules"),
|
|
182
|
+
"javascript": ("njsscan", "semgrep"),
|
|
183
|
+
"typescript": ("njsscan", "semgrep"),
|
|
184
|
+
"ruby": ("rubocop", "semgrep"),
|
|
185
|
+
# No dedicated Go scanner in the floor, but the offline profile's five Go
|
|
186
|
+
# rules do read it: command injection, weak hash, weak cipher, TLS
|
|
187
|
+
# verification disabled, weak random. Verified firing on the shipped Go
|
|
188
|
+
# fixture.
|
|
189
|
+
"go": ("gosec", "semgrep"),
|
|
190
|
+
# Likewise Java: five offline rules. D12 is about PMD and SpotBugs not
|
|
191
|
+
# providing a *dedicated* scanner, which is a statement about depth.
|
|
192
|
+
"java": ("semgrep",),
|
|
193
|
+
# Nothing in the floor reads shell for code defects — no entry to name.
|
|
194
|
+
"shell": (),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
#: File extension -> the language `LANGUAGE_SCANNERS` keys on.
|
|
199
|
+
#:
|
|
200
|
+
#: Only extensions whose language this floor claims to scan for *code*
|
|
201
|
+
#: defects. `.yaml`, `.json`, `Dockerfile` and `.md` are deliberately absent:
|
|
202
|
+
#: they are covered on other axes by checkov and trivy, and mapping them to a
|
|
203
|
+
#: language would make PARTIAL the permanent state of every repository.
|
|
204
|
+
_EXTENSION_LANGUAGE: dict[str, str] = {
|
|
205
|
+
".py": "python",
|
|
206
|
+
".js": "javascript",
|
|
207
|
+
".mjs": "javascript",
|
|
208
|
+
".cjs": "javascript",
|
|
209
|
+
".jsx": "javascript",
|
|
210
|
+
".ts": "typescript",
|
|
211
|
+
".tsx": "typescript",
|
|
212
|
+
".rb": "ruby",
|
|
213
|
+
".go": "go",
|
|
214
|
+
".java": "java",
|
|
215
|
+
".sh": "shell",
|
|
216
|
+
".bash": "shell",
|
|
217
|
+
".zsh": "shell",
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def languages_for(extensions: Iterable[str]) -> tuple[str, ...]:
|
|
222
|
+
"""The code-vulnerability languages these extensions represent, sorted.
|
|
223
|
+
|
|
224
|
+
Deduplicated: `.js` and `.mjs` are both JavaScript and the result names it
|
|
225
|
+
once. An extension with no entry is dropped rather than guessed at.
|
|
226
|
+
"""
|
|
227
|
+
return tuple(
|
|
228
|
+
sorted({_EXTENSION_LANGUAGE[ext] for ext in extensions if ext in _EXTENSION_LANGUAGE})
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _unread_languages(languages: Iterable[str], covered_by: set[str]) -> tuple[str, ...]:
|
|
233
|
+
"""Languages present in the tree that no scanner in this run reads.
|
|
234
|
+
|
|
235
|
+
`gin` — 7,146 lines of Go — reported `coverage: complete` and
|
|
236
|
+
`code_vulnerabilities: 5.0` while bandit, njsscan and rubocop found
|
|
237
|
+
nothing in Go source and `gosec` was not in the run. That is P7's own
|
|
238
|
+
falsification condition, and COMPLETE was a fact about the scanner list
|
|
239
|
+
rather than about the tree.
|
|
240
|
+
"""
|
|
241
|
+
out: list[str] = []
|
|
242
|
+
for language in dict.fromkeys(languages):
|
|
243
|
+
readers = LANGUAGE_SCANNERS.get(language)
|
|
244
|
+
if readers is None:
|
|
245
|
+
# Not a code-vulnerability language in this floor's terms.
|
|
246
|
+
continue
|
|
247
|
+
if any(name in covered_by for name in readers):
|
|
248
|
+
continue
|
|
249
|
+
if readers:
|
|
250
|
+
out.append(f"{language}: no scanner that reads it ran ({', '.join(readers)})")
|
|
251
|
+
else:
|
|
252
|
+
# Nothing exists to suggest, so do not invent a name.
|
|
253
|
+
out.append(f"{language}: no scanner in this floor reads it")
|
|
254
|
+
return tuple(out)
|
|
255
|
+
|
|
256
|
+
|
|
153
257
|
def evaluate_coverage(
|
|
154
258
|
executions: Iterable[ScannerExecution],
|
|
155
259
|
required: Iterable[str],
|
|
260
|
+
languages: Iterable[str] = (),
|
|
156
261
|
) -> CoverageReport:
|
|
262
|
+
"""What examined this tree, and whether that was enough to cover it.
|
|
263
|
+
|
|
264
|
+
`languages` names the programming languages actually present in the
|
|
265
|
+
scanned tree. Omitting it keeps the previous behaviour exactly, so every
|
|
266
|
+
existing call site and every older report keeps its meaning.
|
|
267
|
+
"""
|
|
157
268
|
executions = tuple(executions)
|
|
158
269
|
required = tuple(dict.fromkeys(required))
|
|
159
270
|
by_name = worst_by_name(executions)
|
|
@@ -188,8 +299,20 @@ def evaluate_coverage(
|
|
|
188
299
|
if execution.outcome is ScannerOutcome.UNVERIFIED
|
|
189
300
|
)
|
|
190
301
|
|
|
302
|
+
covered_by = {
|
|
303
|
+
execution.name for execution in by_name.values() if execution.outcome in COVERING_OUTCOMES
|
|
304
|
+
}
|
|
305
|
+
unread = _unread_languages(languages, covered_by)
|
|
306
|
+
|
|
191
307
|
if failures:
|
|
192
308
|
status = CoverageStatus.FAILED
|
|
309
|
+
elif unread:
|
|
310
|
+
# PARTIAL, not FAILED. PARTIAL withholds the *verified* grade and
|
|
311
|
+
# leaves `require_scanners` alone, which is how a partially covered
|
|
312
|
+
# repository already behaves. FAILED would refuse to audit anything
|
|
313
|
+
# containing a shell script. An operator who wants the stricter
|
|
314
|
+
# reading of P7 has `require_scanners` for it.
|
|
315
|
+
status = CoverageStatus.PARTIAL
|
|
193
316
|
elif any(
|
|
194
317
|
execution.outcome not in (*COVERING_OUTCOMES, ScannerOutcome.NOT_APPLICABLE)
|
|
195
318
|
for execution in executions
|
|
@@ -204,4 +327,5 @@ def evaluate_coverage(
|
|
|
204
327
|
unverified=unverified,
|
|
205
328
|
executions=executions,
|
|
206
329
|
failures=tuple(failures),
|
|
330
|
+
unread_languages=unread,
|
|
207
331
|
)
|
|
@@ -723,8 +723,8 @@ def score(
|
|
|
723
723
|
# one state, and only one of them was closed.
|
|
724
724
|
nothing_scanned = loc_scanned <= 0
|
|
725
725
|
for cat in Category:
|
|
726
|
-
|
|
727
|
-
per_category_count[cat] =
|
|
726
|
+
reported = [f for f in findings if f.category == cat and not f.suppressed]
|
|
727
|
+
per_category_count[cat] = len(reported)
|
|
728
728
|
# A finding *is* evidence the category was measurable, whatever the
|
|
729
729
|
# scanner inventory says — otherwise a tool reporting outside its
|
|
730
730
|
# declared domain would have its findings graded as unmeasured. The
|
|
@@ -732,7 +732,28 @@ def score(
|
|
|
732
732
|
# on a tree that counted no lines (a credential in a `.env`, a
|
|
733
733
|
# misconfiguration in a bare dotfile — LOC counts only configured
|
|
734
734
|
# source extensions) is still evidence, and is still graded.
|
|
735
|
-
|
|
735
|
+
#
|
|
736
|
+
# But only a finding that could *move* the score is evidence that a
|
|
737
|
+
# score may be produced. A weightless one cannot: `INFORMATIONAL` is
|
|
738
|
+
# 0.0 in `SEVERITY_WEIGHT`, so it contributes nothing to the subtotal
|
|
739
|
+
# and the category grades a perfect 5.0 on the strength of its own
|
|
740
|
+
# presence.
|
|
741
|
+
#
|
|
742
|
+
# That is how the published 0.12.12 wheel reported 5.00/A+ on an empty
|
|
743
|
+
# tree. A clean install ships no scanners, every adapter emitted its
|
|
744
|
+
# `{name}.tool_unavailable` notice — POLICY_DOCS, INFORMATIONAL — and
|
|
745
|
+
# that notice graded its own category, which then became the overall:
|
|
746
|
+
# `{...all null..., "policy_docs": 5.0}`. D33 was right about scanner
|
|
747
|
+
# findings and wrong about control findings: a notice saying "this tool
|
|
748
|
+
# could not run" is evidence of the opposite of examination.
|
|
749
|
+
#
|
|
750
|
+
# Deliberately expressed as weight rather than as a list of control
|
|
751
|
+
# rule ids. `scoring` must not know about the scanner layer (see this
|
|
752
|
+
# module's own boundary note), the suffixes are four strings that would
|
|
753
|
+
# drift, and the real property is simply "could this have changed the
|
|
754
|
+
# number".
|
|
755
|
+
evidence = sum(1 for f in reported if SEVERITY_WEIGHT[f.severity] > 0.0)
|
|
756
|
+
if evidence == 0 and (nothing_scanned or cat not in measured):
|
|
736
757
|
per_category[cat] = None
|
|
737
758
|
continue
|
|
738
759
|
subtotal = category_subtotal(findings, cat)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/requires.txt
RENAMED
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_agent.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/capabilities.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/data/standards.yaml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/instructions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/__init__.py
RENAMED
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_exclusions.py
RENAMED
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_execution.py
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/_resolution.py
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/floor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/scanners/osv_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.12 → secure_code_agent-0.13.1}/src/secure_code_audit/suppressions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|