secure-code-agent 0.12.4__tar.gz → 0.12.6__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.4/src/secure_code_agent.egg-info → secure_code_agent-0.12.6}/PKG-INFO +2 -2
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/README.md +1 -1
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6/src/secure_code_agent.egg-info}/PKG-INFO +2 -2
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/SOURCES.txt +1 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/__init__.py +1 -1
- secure_code_agent-0.12.6/src/secure_code_audit/capabilities.py +137 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/cli.py +106 -3
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/config.py +38 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/osv_scanner.py +14 -2
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/LICENSE +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/pyproject.toml +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/setup.cfg +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/requires.txt +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/baseline.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/demo.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/findings.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/git_tools.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/history.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/mcp_server.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/pillar.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/practice.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/remediation.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/renderers.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/sarif.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanner_status.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/base.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scoring.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/standards.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/suppressions.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/triage.py +0 -0
- {secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/verify.py +0 -0
{secure_code_agent-0.12.4/src/secure_code_agent.egg-info → secure_code_agent-0.12.6}/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.6
|
|
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
|
|
@@ -513,7 +513,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
513
513
|
## GitHub Action
|
|
514
514
|
|
|
515
515
|
```yaml
|
|
516
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
516
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.6
|
|
517
517
|
with:
|
|
518
518
|
config: secure-code-agent.json
|
|
519
519
|
fail-on-gate: true
|
|
@@ -465,7 +465,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
465
465
|
## GitHub Action
|
|
466
466
|
|
|
467
467
|
```yaml
|
|
468
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
468
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.6
|
|
469
469
|
with:
|
|
470
470
|
config: secure-code-agent.json
|
|
471
471
|
fail-on-gate: true
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6/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.6
|
|
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
|
|
@@ -513,7 +513,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
513
513
|
## GitHub Action
|
|
514
514
|
|
|
515
515
|
```yaml
|
|
516
|
-
- uses: marshallguillory86/secure-code-agent@v0.12.
|
|
516
|
+
- uses: marshallguillory86/secure-code-agent@v0.12.6
|
|
517
517
|
with:
|
|
518
518
|
config: secure-code-agent.json
|
|
519
519
|
fail-on-gate: true
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
@@ -9,6 +9,7 @@ src/secure_code_agent.egg-info/requires.txt
|
|
|
9
9
|
src/secure_code_agent.egg-info/top_level.txt
|
|
10
10
|
src/secure_code_audit/__init__.py
|
|
11
11
|
src/secure_code_audit/baseline.py
|
|
12
|
+
src/secure_code_audit/capabilities.py
|
|
12
13
|
src/secure_code_audit/cli.py
|
|
13
14
|
src/secure_code_audit/config.py
|
|
14
15
|
src/secure_code_audit/demo.py
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""What a project declares it does, and the findings that follow from it.
|
|
2
|
+
|
|
3
|
+
Some findings are not defects and never will be. A tool that runs external
|
|
4
|
+
analyzers imports `subprocess` and spawns them; a tool that reads analyzer
|
|
5
|
+
output parses XML it did not write. Bandit reports both, correctly — the
|
|
6
|
+
observation is true. What was missing is any way for the project to say "yes,
|
|
7
|
+
that is what this is", so the same findings were reported as defects on every
|
|
8
|
+
run forever.
|
|
9
|
+
|
|
10
|
+
The instruments that existed could only hide them. `exclude_patterns` stops
|
|
11
|
+
the scan. `extra_args: --skip` turns the check off. Both leave a report that
|
|
12
|
+
says nothing, which is the silence this tool exists to refuse — the same shape
|
|
13
|
+
`_conformance` counts as a defect when it finds `NOSONAR` in someone else's
|
|
14
|
+
tree. `.scignore.yaml` states a reason, but a suppression expires, and an
|
|
15
|
+
architectural fact does not stop being true in a year; renewing it annually is
|
|
16
|
+
a ritual that teaches people to renew rituals.
|
|
17
|
+
|
|
18
|
+
A declaration is different from all three. The operator states, in the config,
|
|
19
|
+
what the project does. Findings consistent with that declaration are routed to
|
|
20
|
+
their own axis: **counted, listed in the report, and not scored**. Nothing is
|
|
21
|
+
hidden, nothing expires, and a subprocess call written next year is already
|
|
22
|
+
covered rather than generating a new exception.
|
|
23
|
+
|
|
24
|
+
**Declared, never inferred.** The tool does not decide that a project looks
|
|
25
|
+
like it spawns processes. It is told, in a file a reviewer reads, and the
|
|
26
|
+
report names the declaration next to the findings it accounts for. Inference
|
|
27
|
+
here would make the grade depend on a guess about the codebase, which is the
|
|
28
|
+
property this rubric does not have anywhere else.
|
|
29
|
+
|
|
30
|
+
**A declaration is falsifiable.** Declaring a capability the project does not
|
|
31
|
+
exercise is itself reported (`unexercised`), so the config cannot be padded
|
|
32
|
+
with pre-emptive declarations against findings that might arrive later.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from collections.abc import Iterable
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
|
|
40
|
+
from secure_code_audit.findings import Finding
|
|
41
|
+
|
|
42
|
+
#: Capability name -> the rule ids whose finding *is* that capability being
|
|
43
|
+
#: exercised. Deterministic and enumerated rather than pattern-matched: a
|
|
44
|
+
#: reader can see exactly which observations a declaration accounts for, and
|
|
45
|
+
#: a rule absent from this table is never routed by it.
|
|
46
|
+
#:
|
|
47
|
+
#: Scoped narrowly on purpose. `spawns_processes` covers the reports that say
|
|
48
|
+
#: "this code starts a process", not every report that happens to occur in a
|
|
49
|
+
#: file that also starts one. B602 (`shell=True`) is deliberately **not**
|
|
50
|
+
#: here: spawning a process is architecture, handing a string to a shell is a
|
|
51
|
+
#: decision, and a project that declares the former has not excused the latter.
|
|
52
|
+
CAPABILITY_RULES: dict[str, frozenset[str]] = {
|
|
53
|
+
"spawns_processes": frozenset(
|
|
54
|
+
{
|
|
55
|
+
"B404", # import subprocess
|
|
56
|
+
"B603", # subprocess call without shell
|
|
57
|
+
"B606", # start process with no shell
|
|
58
|
+
"B607", # start process with a partial executable path
|
|
59
|
+
}
|
|
60
|
+
),
|
|
61
|
+
"parses_untrusted_xml": frozenset(
|
|
62
|
+
{
|
|
63
|
+
"B313", # xml.etree.cElementTree
|
|
64
|
+
"B314", # xml.etree.ElementTree
|
|
65
|
+
"B405", # import xml.etree
|
|
66
|
+
"B406", # import xml.sax
|
|
67
|
+
"B408", # import xml.minidom
|
|
68
|
+
"B409", # import xml.pulldom
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
"fetches_remote_urls": frozenset(
|
|
72
|
+
{
|
|
73
|
+
"B310", # urllib.request.urlopen
|
|
74
|
+
}
|
|
75
|
+
),
|
|
76
|
+
"uses_nondeterministic_randomness": frozenset(
|
|
77
|
+
{
|
|
78
|
+
"B311", # random, not suitable for cryptography
|
|
79
|
+
}
|
|
80
|
+
),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#: The axis a declared finding is reported on, prefixed so a reader of the
|
|
84
|
+
#: report can tell a declaration apart from a path-derived side axis.
|
|
85
|
+
AXIS_PREFIX = "declared: "
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class DeclarationReport:
|
|
90
|
+
"""What the declarations accounted for, for the report to state."""
|
|
91
|
+
|
|
92
|
+
#: Capability -> the operator's stated reason, as written in the config.
|
|
93
|
+
declared: dict[str, str]
|
|
94
|
+
#: Capability -> how many findings it accounted for.
|
|
95
|
+
accounted: dict[str, int]
|
|
96
|
+
#: Declared, and nothing matched it. A declaration that describes nothing
|
|
97
|
+
#: this project does is reported rather than ignored, because a config can
|
|
98
|
+
#: otherwise be padded against findings that have not arrived yet.
|
|
99
|
+
unexercised: tuple[str, ...]
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def total_accounted(self) -> int:
|
|
103
|
+
return sum(self.accounted.values())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def unknown_capabilities(declared: Iterable[str]) -> tuple[str, ...]:
|
|
107
|
+
"""Declared names this build has no rules for.
|
|
108
|
+
|
|
109
|
+
A typo in a capability name would otherwise declare nothing and route
|
|
110
|
+
nothing, silently, and the operator would read a report that still scored
|
|
111
|
+
the findings they thought they had accounted for.
|
|
112
|
+
"""
|
|
113
|
+
return tuple(sorted(name for name in declared if name not in CAPABILITY_RULES))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def capability_for(finding: Finding, declared: Iterable[str]) -> str | None:
|
|
117
|
+
"""The declared capability this finding is an instance of, or None."""
|
|
118
|
+
for name in declared:
|
|
119
|
+
if finding.rule_id in CAPABILITY_RULES.get(name, frozenset()):
|
|
120
|
+
return name
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def summarize_declarations(
|
|
125
|
+
declared: dict[str, str], findings: Iterable[Finding]
|
|
126
|
+
) -> DeclarationReport:
|
|
127
|
+
"""Count what each declaration accounted for, and what accounted for nothing."""
|
|
128
|
+
accounted = dict.fromkeys(declared, 0)
|
|
129
|
+
for finding in findings:
|
|
130
|
+
name = capability_for(finding, declared)
|
|
131
|
+
if name is not None:
|
|
132
|
+
accounted[name] += 1
|
|
133
|
+
return DeclarationReport(
|
|
134
|
+
declared=dict(declared),
|
|
135
|
+
accounted=accounted,
|
|
136
|
+
unexercised=tuple(sorted(n for n, count in accounted.items() if count == 0)),
|
|
137
|
+
)
|
|
@@ -30,6 +30,11 @@ from secure_code_audit import history as history_mod
|
|
|
30
30
|
from secure_code_audit import pillar as pillar_mod
|
|
31
31
|
from secure_code_audit import practice as practice_mod
|
|
32
32
|
from secure_code_audit import verify as verify_mod
|
|
33
|
+
from secure_code_audit.capabilities import (
|
|
34
|
+
AXIS_PREFIX,
|
|
35
|
+
capability_for,
|
|
36
|
+
summarize_declarations,
|
|
37
|
+
)
|
|
33
38
|
from secure_code_audit.findings import (
|
|
34
39
|
Category,
|
|
35
40
|
Confidence,
|
|
@@ -454,6 +459,16 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
454
459
|
return "test tree"
|
|
455
460
|
if is_test_path(located, root_for_tests, cfg.docs_patterns):
|
|
456
461
|
return "documentation"
|
|
462
|
+
# A finding that *is* a declared capability being exercised (D24),
|
|
463
|
+
# checked **after** the path axes. A subprocess call in the test tree
|
|
464
|
+
# is test tree: it is already unscored, and routing it to the
|
|
465
|
+
# declaration instead moved 492 findings off the tree's own axis and
|
|
466
|
+
# made the declaration look like it accounted for work it did not.
|
|
467
|
+
# The declaration is for the shipped source, which is the only place
|
|
468
|
+
# the path axes leave on the primary axis.
|
|
469
|
+
declared_as = capability_for(finding, cfg.capabilities)
|
|
470
|
+
if declared_as is not None:
|
|
471
|
+
return AXIS_PREFIX + declared_as
|
|
457
472
|
return "primary"
|
|
458
473
|
|
|
459
474
|
# `all_findings` keeps meaning *all* of them. Rebinding it to the primary
|
|
@@ -504,13 +519,49 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
504
519
|
# named — a secret matters wherever it lives, a test fixture's HIGH code
|
|
505
520
|
# smell does not, and feeding a deliberately-vulnerable fixture tree to
|
|
506
521
|
# `fail_on_severity` would fail every build in the corpus.
|
|
507
|
-
|
|
522
|
+
# Every axis whose name carries the declaration prefix, in the order the
|
|
523
|
+
# operator declared them so the report is stable across runs.
|
|
524
|
+
declared_axes = {
|
|
525
|
+
AXIS_PREFIX + name: path_axes.get(AXIS_PREFIX + name, []) for name in cfg.capabilities
|
|
526
|
+
}
|
|
527
|
+
# Declared axes gate on the same terms as the path axes: a secret is a
|
|
528
|
+
# secret wherever it is found, and declaring that this project spawns
|
|
529
|
+
# processes does not excuse a credential sitting next to one.
|
|
530
|
+
gated = gate_set(
|
|
531
|
+
primary_findings,
|
|
532
|
+
(test_findings, docs_findings, *declared_axes.values()),
|
|
533
|
+
cfg.gates,
|
|
534
|
+
)
|
|
508
535
|
measurable = _measurable_categories(executions, scored_findings)
|
|
536
|
+
# Counted over what was actually *routed*, not over every finding whose
|
|
537
|
+
# rule id matches. A subprocess call in the test tree is test tree, and
|
|
538
|
+
# counting it here made the disclosure claim 554 findings while the axis
|
|
539
|
+
# beside it showed 62 — two numbers about the same thing, in the same
|
|
540
|
+
# report, disagreeing by an order of magnitude.
|
|
541
|
+
declarations = summarize_declarations(
|
|
542
|
+
cfg.capabilities,
|
|
543
|
+
[f for found in declared_axes.values() for f in found],
|
|
544
|
+
)
|
|
509
545
|
score = score_findings(scored_findings, loc, measurable)
|
|
546
|
+
# What the declarations bought, stated as a number rather than left for a
|
|
547
|
+
# reader to reconstruct (D24). A declaration moves the grade — that is its
|
|
548
|
+
# purpose, and `exclude_patterns` has always been able to move it further
|
|
549
|
+
# while leaving no trace at all. The difference is disclosure: this reports
|
|
550
|
+
# the score the same tree earns when the declarations are disregarded, so a
|
|
551
|
+
# project that declares its way up a band has to show that on its own
|
|
552
|
+
# report. Computed only when something was actually accounted for, so an
|
|
553
|
+
# undeclared repository's output is unchanged.
|
|
554
|
+
undeclared_score = None
|
|
555
|
+
if declarations.total_accounted:
|
|
556
|
+
undeclared_findings = list(scored_findings) + [
|
|
557
|
+
f for found in declared_axes.values() for f in found if not f.suppressed
|
|
558
|
+
]
|
|
559
|
+
undeclared_score = score_findings(undeclared_findings, loc, measurable)
|
|
510
560
|
axes = (
|
|
511
561
|
summarize_axis("test tree", test_findings, test_loc),
|
|
512
562
|
summarize_axis("documentation", docs_findings, docs_loc or None),
|
|
513
563
|
summarize_axis("dependencies", dependency_findings),
|
|
564
|
+
*(summarize_axis(name, found) for name, found in declared_axes.items()),
|
|
514
565
|
)
|
|
515
566
|
# Naming an import on the command line asserts that it contributes coverage,
|
|
516
567
|
# so a broken one fails the gate even if no config requires that scanner.
|
|
@@ -615,7 +666,18 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
615
666
|
)
|
|
616
667
|
sys.stdout.write("\n")
|
|
617
668
|
else:
|
|
618
|
-
_print_summary(
|
|
669
|
+
_print_summary(
|
|
670
|
+
verdict,
|
|
671
|
+
score,
|
|
672
|
+
gate,
|
|
673
|
+
ran,
|
|
674
|
+
unavailable,
|
|
675
|
+
coverage,
|
|
676
|
+
paths,
|
|
677
|
+
axes,
|
|
678
|
+
trend_line,
|
|
679
|
+
_declaration_delta(score, undeclared_score, declarations),
|
|
680
|
+
)
|
|
619
681
|
|
|
620
682
|
return _exit_code(args, gate, all_findings)
|
|
621
683
|
|
|
@@ -1139,8 +1201,42 @@ def _print_install_guidance(unavailable: list[str]) -> None:
|
|
|
1139
1201
|
print(" (or --preflight to check the whole floor before a run)")
|
|
1140
1202
|
|
|
1141
1203
|
|
|
1204
|
+
def _declaration_delta(score, undeclared_score, declarations) -> str | None:
|
|
1205
|
+
"""One line naming what the declarations accounted for, and what it cost.
|
|
1206
|
+
|
|
1207
|
+
`exclude_patterns` can move a grade further than this and says nothing at
|
|
1208
|
+
all. The difference this mechanism is meant to hold is disclosure, so the
|
|
1209
|
+
number a reader would otherwise have to reconstruct is printed for them.
|
|
1210
|
+
"""
|
|
1211
|
+
if undeclared_score is None or not declarations.total_accounted:
|
|
1212
|
+
return None
|
|
1213
|
+
named = ", ".join(
|
|
1214
|
+
f"{name} ({count})" for name, count in sorted(declarations.accounted.items()) if count
|
|
1215
|
+
)
|
|
1216
|
+
line = (
|
|
1217
|
+
f"declared: {declarations.total_accounted} finding(s) accounted for by "
|
|
1218
|
+
f"{named}; without declarations "
|
|
1219
|
+
f"{undeclared_score.overall:.2f} ({undeclared_score.letter})"
|
|
1220
|
+
)
|
|
1221
|
+
if declarations.unexercised:
|
|
1222
|
+
# A declaration that matched nothing describes something this project
|
|
1223
|
+
# does not do. Named rather than dropped, so a config cannot be padded
|
|
1224
|
+
# against findings that have not arrived yet.
|
|
1225
|
+
line += f"; unexercised: {', '.join(declarations.unexercised)}"
|
|
1226
|
+
return line
|
|
1227
|
+
|
|
1228
|
+
|
|
1142
1229
|
def _print_summary(
|
|
1143
|
-
verdict,
|
|
1230
|
+
verdict,
|
|
1231
|
+
score,
|
|
1232
|
+
gate,
|
|
1233
|
+
ran,
|
|
1234
|
+
unavailable,
|
|
1235
|
+
coverage,
|
|
1236
|
+
paths,
|
|
1237
|
+
axes=(),
|
|
1238
|
+
trend=None,
|
|
1239
|
+
declaration_delta=None,
|
|
1144
1240
|
) -> None:
|
|
1145
1241
|
# "PASS" is a claim that something was checked. With no gate configured
|
|
1146
1242
|
# nothing was, and saying so is the difference between a report and a
|
|
@@ -1162,6 +1258,13 @@ def _print_summary(
|
|
|
1162
1258
|
for axis in axes or ():
|
|
1163
1259
|
if axis.count or axis.loc:
|
|
1164
1260
|
print(f" {axis.headline()}")
|
|
1261
|
+
# What the declarations bought, next to the axes that show what they
|
|
1262
|
+
# accounted for. Printed only when they accounted for something, and
|
|
1263
|
+
# printed even when the two scores land in the same band — "no change"
|
|
1264
|
+
# is the reassuring case and is exactly what a reader should be able to
|
|
1265
|
+
# see without recomputing it.
|
|
1266
|
+
if declaration_delta:
|
|
1267
|
+
print(f" {declaration_delta}")
|
|
1165
1268
|
print(f" scanners run: {', '.join(ran) if ran else '(none)'}")
|
|
1166
1269
|
coverage_line = f" coverage: {coverage.status.value.upper()}"
|
|
1167
1270
|
if coverage.unverified:
|
|
@@ -11,6 +11,11 @@ from dataclasses import dataclass, field
|
|
|
11
11
|
from pathlib import Path, PurePosixPath
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from secure_code_audit.capabilities import (
|
|
15
|
+
CAPABILITY_RULES,
|
|
16
|
+
unknown_capabilities,
|
|
17
|
+
)
|
|
18
|
+
|
|
14
19
|
DEFAULT_CONFIG_PATH = Path("secure-code-agent.json")
|
|
15
20
|
|
|
16
21
|
DEFAULT_EXCLUDES: tuple[str, ...] = (
|
|
@@ -245,6 +250,13 @@ class Config:
|
|
|
245
250
|
#: carries example JWTs that scored as critical secrets and drove it to F.
|
|
246
251
|
#: Reported and gated like the test tree, never scored as code condition.
|
|
247
252
|
docs_patterns: tuple[str, ...] = DEFAULT_DOCS_PATTERNS
|
|
253
|
+
#: What this project declares it does, as ``name -> reason`` (D24). A
|
|
254
|
+
#: finding that *is* a declared capability being exercised is reported on
|
|
255
|
+
#: its own axis rather than scored: a tool that runs analyzers spawns
|
|
256
|
+
#: processes, and reporting that as a defect on every run forever is the
|
|
257
|
+
#: observation being true and useless. Declared, never inferred, and
|
|
258
|
+
#: falsifiable — a declaration nothing matches is reported as unexercised.
|
|
259
|
+
capabilities: dict[str, str] = field(default_factory=dict)
|
|
248
260
|
scanners: dict[str, ScannerConfig] = field(default_factory=dict)
|
|
249
261
|
severity_overrides: dict[str, str] = field(default_factory=dict)
|
|
250
262
|
category_overrides: dict[str, str] = field(default_factory=dict)
|
|
@@ -398,6 +410,7 @@ _KNOWN_KEYS = frozenset(
|
|
|
398
410
|
"$schema",
|
|
399
411
|
"version",
|
|
400
412
|
"paths",
|
|
413
|
+
"capabilities",
|
|
401
414
|
"scanners",
|
|
402
415
|
"severity_overrides",
|
|
403
416
|
"category_overrides",
|
|
@@ -442,6 +455,31 @@ def _from_dict(raw: dict[str, Any]) -> Config:
|
|
|
442
455
|
if "docs_patterns" in paths:
|
|
443
456
|
cfg.docs_patterns = tuple(_string_list(paths["docs_patterns"], "paths.docs_patterns"))
|
|
444
457
|
|
|
458
|
+
capabilities_raw = raw.get("capabilities", {})
|
|
459
|
+
if not isinstance(capabilities_raw, dict):
|
|
460
|
+
raise ValueError("capabilities must be a JSON object of name -> reason")
|
|
461
|
+
for name, reason in capabilities_raw.items():
|
|
462
|
+
if not isinstance(name, str) or not name:
|
|
463
|
+
raise ValueError("capability names must be non-empty strings")
|
|
464
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
465
|
+
# A declaration without a reason is the thing this is meant to
|
|
466
|
+
# replace. The reason is what a reviewer reads to decide whether
|
|
467
|
+
# the declaration is still true, so an empty one is refused here
|
|
468
|
+
# rather than rendered as a blank line in the report.
|
|
469
|
+
raise ValueError(f"capabilities.{name} must state a non-empty reason")
|
|
470
|
+
unknown = unknown_capabilities(capabilities_raw)
|
|
471
|
+
if unknown:
|
|
472
|
+
# A typo would declare nothing, route nothing, and score the findings
|
|
473
|
+
# the operator believed they had accounted for -- silently, which is
|
|
474
|
+
# the failure mode this whole mechanism exists to remove.
|
|
475
|
+
raise ValueError(
|
|
476
|
+
"unknown capabilities: "
|
|
477
|
+
+ ", ".join(unknown)
|
|
478
|
+
+ "; known names are "
|
|
479
|
+
+ ", ".join(sorted(CAPABILITY_RULES))
|
|
480
|
+
)
|
|
481
|
+
cfg.capabilities = dict(capabilities_raw)
|
|
482
|
+
|
|
445
483
|
scanners_raw = raw.get("scanners", {})
|
|
446
484
|
if not isinstance(scanners_raw, dict):
|
|
447
485
|
raise ValueError("scanners must be a JSON object")
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/osv_scanner.py
RENAMED
|
@@ -20,6 +20,13 @@ from secure_code_audit.findings import Category, Confidence, Finding, Severity
|
|
|
20
20
|
from secure_code_audit.scanner_status import ScanResult
|
|
21
21
|
from secure_code_audit.scanners.base import Scanner
|
|
22
22
|
|
|
23
|
+
#: osv-scanner's exit code for "No package sources found" — no lockfile,
|
|
24
|
+
#: manifest or SBOM anywhere under the target. Nothing to read is not a
|
|
25
|
+
#: failure to read, so it becomes NOT_APPLICABLE (D23). The tool's own
|
|
26
|
+
#: `--allow-no-lockfiles` would turn this into exit 0 instead, which would
|
|
27
|
+
#: report a completed run over nothing; an outcome that says so is better.
|
|
28
|
+
_NO_PACKAGE_SOURCES = 128
|
|
29
|
+
|
|
23
30
|
_OSV_SEVERITY: dict[str, Severity] = {
|
|
24
31
|
"CRITICAL": Severity.CRITICAL,
|
|
25
32
|
"HIGH": Severity.HIGH,
|
|
@@ -53,13 +60,18 @@ class OsvScanner(Scanner):
|
|
|
53
60
|
]
|
|
54
61
|
args.extend(sc_cfg.extra_args)
|
|
55
62
|
|
|
56
|
-
# osv-scanner exits
|
|
57
|
-
# 128
|
|
63
|
+
# osv-scanner exits 0 on clean, 1 on findings, 127 on general failure,
|
|
64
|
+
# and 128 when it found nothing to read. 128 was documented here as an
|
|
65
|
+
# internal error and mapped to FAILED — see D23.
|
|
58
66
|
r = self._exec(
|
|
59
67
|
args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds, allowed_exits=(0, 1)
|
|
60
68
|
)
|
|
61
69
|
if r.returncode == 124:
|
|
62
70
|
return self.timed_out(target, f"osv-scanner timed out: {r.stderr[:200]}")
|
|
71
|
+
if r.returncode == _NO_PACKAGE_SOURCES:
|
|
72
|
+
return self.not_applicable(
|
|
73
|
+
target, "osv-scanner found no package sources (lockfile, manifest or SBOM) to scan"
|
|
74
|
+
)
|
|
63
75
|
if r.returncode not in (0, 1):
|
|
64
76
|
return self.failed(target, f"osv-scanner failed: {r.stderr[:300]}")
|
|
65
77
|
if not r.stdout.strip():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/requires.txt
RENAMED
|
File without changes
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_agent.egg-info/top_level.txt
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
|
|
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.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanner_status.py
RENAMED
|
File without changes
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/src/secure_code_audit/scanners/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.12.4 → secure_code_agent-0.12.6}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|