secure-code-agent 0.9.0__tar.gz → 0.11.0__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.9.0/src/secure_code_agent.egg-info → secure_code_agent-0.11.0}/PKG-INFO +31 -3
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/README.md +30 -2
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0/src/secure_code_agent.egg-info}/PKG-INFO +31 -3
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/SOURCES.txt +1 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/__init__.py +1 -1
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/cli.py +147 -4
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/config.py +128 -3
- secure_code_agent-0.11.0/src/secure_code_audit/demo.py +118 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/git_tools.py +65 -1
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/pillar.py +53 -2
- secure_code_agent-0.11.0/src/secure_code_audit/remediation.py +332 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/base.py +28 -9
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scoring.py +141 -8
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/standards.py +60 -3
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/triage.py +129 -1
- secure_code_agent-0.9.0/src/secure_code_audit/remediation.py +0 -303
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/LICENSE +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/pyproject.toml +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/setup.cfg +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/requires.txt +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/baseline.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/findings.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/history.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/practice.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/renderers.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/sarif.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanner_status.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/suppressions.py +0 -0
- {secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_audit/verify.py +0 -0
{secure_code_agent-0.9.0/src/secure_code_agent.egg-info → secure_code_agent-0.11.0}/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.11.0
|
|
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
|
|
@@ -62,6 +62,34 @@ The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/m
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
+
## Try it in one command
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install 'secure-code-agent[python-scanners]'
|
|
69
|
+
secure-code-agent --demo
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--demo` generates a small application carrying seven real defects — SQL
|
|
73
|
+
injection, command injection, unsafe deserialization, a weak hash, dynamic
|
|
74
|
+
evaluation — audits it, and writes the work order you would hand an agent. The
|
|
75
|
+
tree is generated at runtime into a temp directory, so installing this tool
|
|
76
|
+
never puts vulnerable source on your disk.
|
|
77
|
+
|
|
78
|
+
To audit something real:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
secure-code-agent /path/to/repo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Any scanner that is missing is named, with the command to install it. This
|
|
85
|
+
tool never installs anything itself — naming a command is not running one —
|
|
86
|
+
and it will not grade what it could not examine. `--preflight` checks the
|
|
87
|
+
whole floor before a run.
|
|
88
|
+
|
|
89
|
+
A pure-Python repository requires six of the ten floor scanners; the Ruby,
|
|
90
|
+
JavaScript and container tools are *not applicable* rather than missing, so a
|
|
91
|
+
single-language project is never permanently incomplete.
|
|
92
|
+
|
|
65
93
|
## Why this exists
|
|
66
94
|
|
|
67
95
|
AI coding agents ship code at human-review-saturating speed. Point them at a security finding and the documented anti-patterns are:
|
|
@@ -442,7 +470,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
442
470
|
## GitHub Action
|
|
443
471
|
|
|
444
472
|
```yaml
|
|
445
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
473
|
+
- uses: marshallguillory86/secure-code-agent@v0.11.0
|
|
446
474
|
with:
|
|
447
475
|
config: secure-code-agent.json
|
|
448
476
|
fail-on-gate: true
|
|
@@ -508,7 +536,7 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
508
536
|
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
509
537
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
510
538
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
511
|
-
- [`docs/release-blockers.md`](docs/release-blockers.md) —
|
|
539
|
+
- [`docs/release-blockers.md`](docs/release-blockers.md) — the v0.3.0 release blockers, all closed (historical)
|
|
512
540
|
- [`docs/standards.md`](docs/standards.md) — NIST SSDF / OWASP / CWE / Scorecard / SARIF citations
|
|
513
541
|
- [`docs/scoring.md`](docs/scoring.md) — Weighting model + worked examples
|
|
514
542
|
- [`docs/scanners.md`](docs/scanners.md) — Per-scanner integrations + caveats
|
|
@@ -16,6 +16,34 @@ The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/m
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Try it in one command
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install 'secure-code-agent[python-scanners]'
|
|
23
|
+
secure-code-agent --demo
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`--demo` generates a small application carrying seven real defects — SQL
|
|
27
|
+
injection, command injection, unsafe deserialization, a weak hash, dynamic
|
|
28
|
+
evaluation — audits it, and writes the work order you would hand an agent. The
|
|
29
|
+
tree is generated at runtime into a temp directory, so installing this tool
|
|
30
|
+
never puts vulnerable source on your disk.
|
|
31
|
+
|
|
32
|
+
To audit something real:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
secure-code-agent /path/to/repo
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Any scanner that is missing is named, with the command to install it. This
|
|
39
|
+
tool never installs anything itself — naming a command is not running one —
|
|
40
|
+
and it will not grade what it could not examine. `--preflight` checks the
|
|
41
|
+
whole floor before a run.
|
|
42
|
+
|
|
43
|
+
A pure-Python repository requires six of the ten floor scanners; the Ruby,
|
|
44
|
+
JavaScript and container tools are *not applicable* rather than missing, so a
|
|
45
|
+
single-language project is never permanently incomplete.
|
|
46
|
+
|
|
19
47
|
## Why this exists
|
|
20
48
|
|
|
21
49
|
AI coding agents ship code at human-review-saturating speed. Point them at a security finding and the documented anti-patterns are:
|
|
@@ -396,7 +424,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
396
424
|
## GitHub Action
|
|
397
425
|
|
|
398
426
|
```yaml
|
|
399
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
427
|
+
- uses: marshallguillory86/secure-code-agent@v0.11.0
|
|
400
428
|
with:
|
|
401
429
|
config: secure-code-agent.json
|
|
402
430
|
fail-on-gate: true
|
|
@@ -462,7 +490,7 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
462
490
|
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
463
491
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
464
492
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
465
|
-
- [`docs/release-blockers.md`](docs/release-blockers.md) —
|
|
493
|
+
- [`docs/release-blockers.md`](docs/release-blockers.md) — the v0.3.0 release blockers, all closed (historical)
|
|
466
494
|
- [`docs/standards.md`](docs/standards.md) — NIST SSDF / OWASP / CWE / Scorecard / SARIF citations
|
|
467
495
|
- [`docs/scoring.md`](docs/scoring.md) — Weighting model + worked examples
|
|
468
496
|
- [`docs/scanners.md`](docs/scanners.md) — Per-scanner integrations + caveats
|
{secure_code_agent-0.9.0 → secure_code_agent-0.11.0/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.11.0
|
|
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
|
|
@@ -62,6 +62,34 @@ The sibling of [`maintainability-agent`](https://github.com/marshallguillory86/m
|
|
|
62
62
|
|
|
63
63
|
---
|
|
64
64
|
|
|
65
|
+
## Try it in one command
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install 'secure-code-agent[python-scanners]'
|
|
69
|
+
secure-code-agent --demo
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`--demo` generates a small application carrying seven real defects — SQL
|
|
73
|
+
injection, command injection, unsafe deserialization, a weak hash, dynamic
|
|
74
|
+
evaluation — audits it, and writes the work order you would hand an agent. The
|
|
75
|
+
tree is generated at runtime into a temp directory, so installing this tool
|
|
76
|
+
never puts vulnerable source on your disk.
|
|
77
|
+
|
|
78
|
+
To audit something real:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
secure-code-agent /path/to/repo
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Any scanner that is missing is named, with the command to install it. This
|
|
85
|
+
tool never installs anything itself — naming a command is not running one —
|
|
86
|
+
and it will not grade what it could not examine. `--preflight` checks the
|
|
87
|
+
whole floor before a run.
|
|
88
|
+
|
|
89
|
+
A pure-Python repository requires six of the ten floor scanners; the Ruby,
|
|
90
|
+
JavaScript and container tools are *not applicable* rather than missing, so a
|
|
91
|
+
single-language project is never permanently incomplete.
|
|
92
|
+
|
|
65
93
|
## Why this exists
|
|
66
94
|
|
|
67
95
|
AI coding agents ship code at human-review-saturating speed. Point them at a security finding and the documented anti-patterns are:
|
|
@@ -442,7 +470,7 @@ For agents that support invokable skills, this repo ships a portable skill under
|
|
|
442
470
|
## GitHub Action
|
|
443
471
|
|
|
444
472
|
```yaml
|
|
445
|
-
- uses: marshallguillory86/secure-code-agent@v0.
|
|
473
|
+
- uses: marshallguillory86/secure-code-agent@v0.11.0
|
|
446
474
|
with:
|
|
447
475
|
config: secure-code-agent.json
|
|
448
476
|
fail-on-gate: true
|
|
@@ -508,7 +536,7 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
508
536
|
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
509
537
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
510
538
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
511
|
-
- [`docs/release-blockers.md`](docs/release-blockers.md) —
|
|
539
|
+
- [`docs/release-blockers.md`](docs/release-blockers.md) — the v0.3.0 release blockers, all closed (historical)
|
|
512
540
|
- [`docs/standards.md`](docs/standards.md) — NIST SSDF / OWASP / CWE / Scorecard / SARIF citations
|
|
513
541
|
- [`docs/scoring.md`](docs/scoring.md) — Weighting model + worked examples
|
|
514
542
|
- [`docs/scanners.md`](docs/scanners.md) — Per-scanner integrations + caveats
|
{secure_code_agent-0.9.0 → secure_code_agent-0.11.0}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
@@ -11,6 +11,7 @@ src/secure_code_audit/__init__.py
|
|
|
11
11
|
src/secure_code_audit/baseline.py
|
|
12
12
|
src/secure_code_audit/cli.py
|
|
13
13
|
src/secure_code_audit/config.py
|
|
14
|
+
src/secure_code_audit/demo.py
|
|
14
15
|
src/secure_code_audit/findings.py
|
|
15
16
|
src/secure_code_audit/git_tools.py
|
|
16
17
|
src/secure_code_audit/history.py
|
|
@@ -15,6 +15,7 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
from secure_code_audit import (
|
|
17
17
|
__version__,
|
|
18
|
+
demo,
|
|
18
19
|
instructions,
|
|
19
20
|
remediation,
|
|
20
21
|
renderers,
|
|
@@ -136,9 +137,22 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
136
137
|
"--trust-target-config",
|
|
137
138
|
action="store_true",
|
|
138
139
|
help=(
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
140
|
+
"Treat a config inside the audited tree as if you wrote it. This "
|
|
141
|
+
"grants it TWO things: (1) it may name executables from that tree, "
|
|
142
|
+
"which this host will then run, and (2) it may direct this tool's "
|
|
143
|
+
"outputs, baseline and history to paths outside the tree. Only for "
|
|
144
|
+
"repositories you own. CLI-only by design: a config file cannot "
|
|
145
|
+
"grant itself this."
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
p.add_argument(
|
|
150
|
+
"--demo",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help=(
|
|
153
|
+
"Audit a generated example application instead of a repository, so "
|
|
154
|
+
"a first run produces a real work order without a scanner scavenger "
|
|
155
|
+
"hunt. The tree is written to a temp directory and is never shipped."
|
|
142
156
|
),
|
|
143
157
|
)
|
|
144
158
|
|
|
@@ -160,7 +174,11 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
160
174
|
"--target",
|
|
161
175
|
action="append",
|
|
162
176
|
default=[],
|
|
163
|
-
help=
|
|
177
|
+
help=(
|
|
178
|
+
"Agent name for --init-agent-standards (codex, claude-code, cursor, "
|
|
179
|
+
"copilot, windsurf, generic). NOT the repository to audit — pass that "
|
|
180
|
+
"as a positional path."
|
|
181
|
+
),
|
|
164
182
|
)
|
|
165
183
|
p.add_argument(
|
|
166
184
|
"--instructions-output-dir",
|
|
@@ -183,6 +201,32 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
183
201
|
if args.init_agent_standards:
|
|
184
202
|
return _do_init_standards(args)
|
|
185
203
|
|
|
204
|
+
if getattr(args, "demo", False):
|
|
205
|
+
# `paths` defaults to ["."], so "was a path given" is a comparison
|
|
206
|
+
# against the default rather than a truth test.
|
|
207
|
+
if args.paths != ["."]:
|
|
208
|
+
sys.stderr.write("ERROR: --demo audits its own generated tree; drop the path.\n")
|
|
209
|
+
return 2
|
|
210
|
+
root = demo.build()
|
|
211
|
+
print(demo.describe(root))
|
|
212
|
+
args.paths = [str(root)]
|
|
213
|
+
|
|
214
|
+
if args.target:
|
|
215
|
+
# `--target` names an *agent* for --init-agent-standards, and it reads
|
|
216
|
+
# exactly like the flag for "the repository to audit". It was accepted
|
|
217
|
+
# and silently discarded on an audit run, so `--target /some/repo`
|
|
218
|
+
# audited the current directory instead and reported a clean result
|
|
219
|
+
# for a repository nobody had looked at. Found by using the tool: a
|
|
220
|
+
# 136k-line control was audited from inside itself and appeared to
|
|
221
|
+
# honour the flag, which is the kind of coincidence that keeps a bug.
|
|
222
|
+
sys.stderr.write(
|
|
223
|
+
"ERROR: --target names an agent for --init-agent-standards, not a "
|
|
224
|
+
"repository to audit.\n"
|
|
225
|
+
f" To audit a repository, pass it as a path: "
|
|
226
|
+
f"secure-code-agent {args.target[0]}\n"
|
|
227
|
+
)
|
|
228
|
+
return 2
|
|
229
|
+
|
|
186
230
|
try:
|
|
187
231
|
return _do_preflight(args) if args.preflight else _do_audit(args)
|
|
188
232
|
except ValueError as exc:
|
|
@@ -279,6 +323,8 @@ def _print_preflight(rows: list[dict], unselected: list[str], blocking: list[str
|
|
|
279
323
|
def _do_audit(args: argparse.Namespace) -> int:
|
|
280
324
|
cfg, target, root = _prepare_audit(args)
|
|
281
325
|
_require_configured_gates(args, cfg)
|
|
326
|
+
# Before anything is resolved or written, and before any scanner runs.
|
|
327
|
+
_assert_config_writes_are_contained(cfg, root, target)
|
|
282
328
|
# Resolved before the scan so the run can recognise its own artifacts.
|
|
283
329
|
paths = _resolve_outputs(args, cfg, root)
|
|
284
330
|
|
|
@@ -902,6 +948,102 @@ def _under_root(root: Path, value: str) -> Path:
|
|
|
902
948
|
return path.resolve() if path.is_absolute() else (root / path).resolve()
|
|
903
949
|
|
|
904
950
|
|
|
951
|
+
#: Config keys naming a file this tool writes.
|
|
952
|
+
_WRITE_KEYS = (
|
|
953
|
+
"markdown_path",
|
|
954
|
+
"json_path",
|
|
955
|
+
"sarif_path",
|
|
956
|
+
"comment_path",
|
|
957
|
+
"prompt_path",
|
|
958
|
+
"security_pillar_path",
|
|
959
|
+
"baseline_path",
|
|
960
|
+
"history_path",
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
def _assert_config_writes_are_contained(cfg: config_mod.Config, root: Path, target: Path) -> None:
|
|
965
|
+
"""A tree may not choose where the host writes.
|
|
966
|
+
|
|
967
|
+
D1 draws the line at *executing* what the tree supplies, and that was the
|
|
968
|
+
only boundary implemented. The tree could still choose file *paths*: the
|
|
969
|
+
default configuration is loaded from the audit target, and its
|
|
970
|
+
`outputs.*_path`, baseline and history values were resolved against the
|
|
971
|
+
root with no containment check at all. A repository shipping
|
|
972
|
+
|
|
973
|
+
{"outputs": {"markdown_path": "../../../../.bashrc"}}
|
|
974
|
+
|
|
975
|
+
had an ordinary audit overwrite that file, with the report's own content.
|
|
976
|
+
Arbitrary write is not a lesser thing than arbitrary execute; it is
|
|
977
|
+
usually a slower route to the same place.
|
|
978
|
+
|
|
979
|
+
Three escapes, all closed here and all in the red contract: `..`
|
|
980
|
+
traversal, an absolute path, and a **symlink inside the tree** pointing
|
|
981
|
+
out of it. The third is why this resolves before comparing rather than
|
|
982
|
+
checking the string — `escaped/report.md`, where `escaped` is a symlink
|
|
983
|
+
to the parent, is textually innocent.
|
|
984
|
+
|
|
985
|
+
**Only for a config the tree supplied.** An operator whose config lives
|
|
986
|
+
outside the audited tree keeps full authority over where things go, and
|
|
987
|
+
so does anyone passing `--trust-target-config`. Explicit CLI output flags
|
|
988
|
+
are never touched by this: `--output /tmp/report.md` is the operator
|
|
989
|
+
speaking, and they are checked nowhere in this function.
|
|
990
|
+
|
|
991
|
+
Raises ValueError, so the CLI exits 2 before any scanner runs and before
|
|
992
|
+
a single byte is written.
|
|
993
|
+
"""
|
|
994
|
+
if not config_mod.target_config_is_untrusted(cfg, target):
|
|
995
|
+
return
|
|
996
|
+
for key in _WRITE_KEYS:
|
|
997
|
+
configured = cfg.outputs.get(key)
|
|
998
|
+
if not configured:
|
|
999
|
+
continue
|
|
1000
|
+
destination = _under_root(root, configured)
|
|
1001
|
+
if config_mod.is_within(destination, root):
|
|
1002
|
+
continue
|
|
1003
|
+
raise ValueError(
|
|
1004
|
+
f"outputs.{key} in {cfg.source_path} resolves to {destination}, which is "
|
|
1005
|
+
f"outside the audit root {root}. Configuration supplied by the audited "
|
|
1006
|
+
f"tree may not choose where this tool writes. Pass the path on the "
|
|
1007
|
+
f"command line instead, move the configuration outside the tree, or "
|
|
1008
|
+
f"re-run with --trust-target-config if you wrote this file yourself."
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _print_install_guidance(unavailable: list[str]) -> None:
|
|
1013
|
+
"""Say how to resolve a missing scanner, at the moment it is missing.
|
|
1014
|
+
|
|
1015
|
+
`--preflight` already produced exactly this — one install command per
|
|
1016
|
+
scanner — and the failure path printed only the names. So the first run
|
|
1017
|
+
of a fresh install reported four missing tools and left the operator to
|
|
1018
|
+
find `--preflight` on their own, which is a scavenger hunt wearing a
|
|
1019
|
+
coverage report.
|
|
1020
|
+
|
|
1021
|
+
The guidance is the adapter's own `install_hint`, so it cannot drift from
|
|
1022
|
+
what `--preflight` says. The tool still installs nothing itself; naming
|
|
1023
|
+
the command is not running it.
|
|
1024
|
+
"""
|
|
1025
|
+
from secure_code_audit.scanners import SCANNERS
|
|
1026
|
+
|
|
1027
|
+
def hint_for(name: str) -> str:
|
|
1028
|
+
"""Empty rather than raising. Advice must never break a run, and an
|
|
1029
|
+
imported SARIF can name a scanner this build does not ship."""
|
|
1030
|
+
cls = SCANNERS.get(name)
|
|
1031
|
+
if cls is None:
|
|
1032
|
+
return ""
|
|
1033
|
+
try:
|
|
1034
|
+
return cls().unavailable_fix_hint() or ""
|
|
1035
|
+
except Exception: # noqa: BLE001 — see above
|
|
1036
|
+
return ""
|
|
1037
|
+
|
|
1038
|
+
shown = [(name, hint) for name in unavailable if (hint := hint_for(name))]
|
|
1039
|
+
if not shown:
|
|
1040
|
+
return
|
|
1041
|
+
print(" to resolve:")
|
|
1042
|
+
for name, hint in shown:
|
|
1043
|
+
print(f" {name}: {hint}")
|
|
1044
|
+
print(" (or --preflight to check the whole floor before a run)")
|
|
1045
|
+
|
|
1046
|
+
|
|
905
1047
|
def _print_summary(
|
|
906
1048
|
verdict, score, gate, ran, unavailable, coverage, paths, axes=(), trend=None
|
|
907
1049
|
) -> None:
|
|
@@ -934,6 +1076,7 @@ def _print_summary(
|
|
|
934
1076
|
print(coverage_line)
|
|
935
1077
|
if unavailable:
|
|
936
1078
|
print(f" unavailable: {', '.join(unavailable)}")
|
|
1079
|
+
_print_install_guidance(unavailable)
|
|
937
1080
|
if not gate.passed:
|
|
938
1081
|
for reason in gate.reasons:
|
|
939
1082
|
print(f" ✗ {reason}")
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
|
-
from pathlib import Path
|
|
11
|
+
from pathlib import Path, PurePosixPath
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
DEFAULT_CONFIG_PATH = Path("secure-code-agent.json")
|
|
@@ -37,6 +37,42 @@ DEFAULT_EXCLUDES: tuple[str, ...] = (
|
|
|
37
37
|
"**/npm-shrinkwrap.json",
|
|
38
38
|
"**/pnpm-lock.yaml",
|
|
39
39
|
"**/bun.lockb",
|
|
40
|
+
# --- stored analysis output -------------------------------------------
|
|
41
|
+
#
|
|
42
|
+
# This tool's own output is never its input, *wherever* it is stored.
|
|
43
|
+
#
|
|
44
|
+
# `cli._own_artifacts` already removes the paths the current run is about
|
|
45
|
+
# to write, which is what stopped an audit scoring the report it had just
|
|
46
|
+
# produced. It cannot see a *copy* kept somewhere else, and two
|
|
47
|
+
# independent reports of that landed on the same day:
|
|
48
|
+
#
|
|
49
|
+
# - this repository scanned `calibration/.corpus` — fourteen cloned
|
|
50
|
+
# third-party projects, 556,808 LOC and 550 findings, all about code
|
|
51
|
+
# that is not ours;
|
|
52
|
+
# - `maintainability-agent` scanned `tools/validation/reports/` —
|
|
53
|
+
# 957,219 LOC of stored audit output *about other repositories*,
|
|
54
|
+
# 4,929 findings, which diluted five genuine criticals to an A-.
|
|
55
|
+
#
|
|
56
|
+
# A stored report is the worst possible input: it quotes findings
|
|
57
|
+
# verbatim, including the code snippets and the redacted secrets that
|
|
58
|
+
# produced them, so it manufactures findings about findings and inflates
|
|
59
|
+
# the denominator at the same time.
|
|
60
|
+
#
|
|
61
|
+
# Filename patterns rather than directory names, because the directory is
|
|
62
|
+
# whatever the operator chose and the filenames are ours. Derived from
|
|
63
|
+
# `DEFAULT_OUTPUTS` below so the two cannot drift — see
|
|
64
|
+
# `_own_output_globs`.
|
|
65
|
+
#
|
|
66
|
+
# Deliberately NOT here: `vendor/`, `third_party/` and their kin. Vendored
|
|
67
|
+
# code is deployed code, and excluding it by default would hide real
|
|
68
|
+
# vulnerabilities in exactly the place nobody is reading. Stored analysis
|
|
69
|
+
# output is not code at all; that is the whole difference.
|
|
70
|
+
".secure-code/",
|
|
71
|
+
"**/.secure-code/",
|
|
72
|
+
# maintainability-agent's state and output directory. Same argument: its
|
|
73
|
+
# reports quote findings, and its history is append-only JSONL.
|
|
74
|
+
".maintainability/",
|
|
75
|
+
"**/.maintainability/",
|
|
40
76
|
)
|
|
41
77
|
|
|
42
78
|
#: Conventional test-tree locations across the languages the floor reads.
|
|
@@ -105,8 +141,36 @@ DEFAULT_OUTPUTS: dict[str, str] = {
|
|
|
105
141
|
"baseline_path": "secure-code-baseline.json",
|
|
106
142
|
# Append-only trend. The score's one genuine use is movement over time.
|
|
107
143
|
"history_path": ".secure-code/history.jsonl",
|
|
144
|
+
# The artifact maintainability-agent ingests (D3). Off unless declared,
|
|
145
|
+
# like sarif/json/comment — the default only supplies the name.
|
|
146
|
+
#
|
|
147
|
+
# It was missing here while `_resolve_outputs` read it, so
|
|
148
|
+
# `outputs.security_pillar_path` was a **dead key**: the validation loop
|
|
149
|
+
# below iterates this mapping, so a configured value never reached
|
|
150
|
+
# `cfg.outputs` and the resolver read `None` from it every time. That is
|
|
151
|
+
# the seventh instance of the defect `_resolve_outputs` already
|
|
152
|
+
# describes — "four of the six keys were dead the same way".
|
|
153
|
+
"security_pillar_path": "security-pillar.json",
|
|
108
154
|
}
|
|
109
155
|
|
|
156
|
+
|
|
157
|
+
def _own_output_globs() -> tuple[str, ...]:
|
|
158
|
+
"""Match this tool's default output filenames anywhere in a tree.
|
|
159
|
+
|
|
160
|
+
Kept as a function over `DEFAULT_OUTPUTS` rather than a hand-written list
|
|
161
|
+
so that adding an output cannot leave a file this tool writes readable by
|
|
162
|
+
the next run. `history_path` is already covered by the `.secure-code/`
|
|
163
|
+
directory entries; matching its basename anywhere would be wrong, since
|
|
164
|
+
`history.jsonl` is not a name this project owns.
|
|
165
|
+
"""
|
|
166
|
+
names = {
|
|
167
|
+
PurePosixPath(path).name for key, path in DEFAULT_OUTPUTS.items() if key != "history_path"
|
|
168
|
+
}
|
|
169
|
+
return tuple(sorted(f"**/{name}" for name in names))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
DEFAULT_EXCLUDES = DEFAULT_EXCLUDES + _own_output_globs()
|
|
173
|
+
|
|
110
174
|
_SEVERITIES = {"critical", "high", "medium", "low", "informational"}
|
|
111
175
|
_CATEGORIES = {
|
|
112
176
|
"secrets",
|
|
@@ -251,6 +315,51 @@ def is_within(path: Path, root: Path) -> bool:
|
|
|
251
315
|
return resolved == root_resolved or root_resolved in resolved.parents
|
|
252
316
|
|
|
253
317
|
|
|
318
|
+
def containment_root(target: Path) -> Path:
|
|
319
|
+
"""The directory every containment decision is made against.
|
|
320
|
+
|
|
321
|
+
**A file target's trust boundary is its parent, not the file.** Auditing
|
|
322
|
+
`app.py` meant `is_within(<anything>, app.py)` was false for everything,
|
|
323
|
+
because nothing lives beneath a regular file — so every containment test
|
|
324
|
+
inverted to "allowed" at once. A `secure-code-agent.json` beside the file
|
|
325
|
+
read as *outside* the tree, and `--only-scanners bandit` with an in-tree
|
|
326
|
+
`command` executed a script from the audited repository without
|
|
327
|
+
`--trust-target-config`. The single-file shape silently opted out of the
|
|
328
|
+
guard that D1 exists to hold.
|
|
329
|
+
|
|
330
|
+
Directories are returned unchanged, so the ordinary repository audit is
|
|
331
|
+
unaffected.
|
|
332
|
+
"""
|
|
333
|
+
resolved = target.resolve()
|
|
334
|
+
return resolved if resolved.is_dir() else resolved.parent
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def target_config_is_untrusted(config: Config, target: Path) -> bool:
|
|
338
|
+
"""Was this configuration supplied by the audited tree?
|
|
339
|
+
|
|
340
|
+
Location, not provenance of the flag: a config *inside* the target is
|
|
341
|
+
repository content whether it was found by default discovery or named
|
|
342
|
+
explicitly with `--config`. An operator who means to trust it says so with
|
|
343
|
+
`--trust-target-config`.
|
|
344
|
+
|
|
345
|
+
**This is not the negation of `target_executables_allowed`, and writing it
|
|
346
|
+
as one was a live regression.** They disagree when there is no config file
|
|
347
|
+
at all. For execution, "no config" must still *deny* tree-local
|
|
348
|
+
executables — nothing has authorised them, and an absent file is not
|
|
349
|
+
permission. For writes, "no config" is simply nothing to distrust: the
|
|
350
|
+
built-in defaults are relative names that cannot escape. Collapsing the
|
|
351
|
+
two flipped the execution guard open for every run with no config, which
|
|
352
|
+
`test_defaults_never_allow_executables_from_the_tree` caught immediately.
|
|
353
|
+
|
|
354
|
+
Only `containment_root` is shared.
|
|
355
|
+
"""
|
|
356
|
+
if config.trust_target_config:
|
|
357
|
+
return False
|
|
358
|
+
if config.source_path is None:
|
|
359
|
+
return False # built-in defaults are relative names; nothing can escape
|
|
360
|
+
return is_within(config.source_path, containment_root(target))
|
|
361
|
+
|
|
362
|
+
|
|
254
363
|
def target_executables_allowed(config: Config, target: Path) -> bool:
|
|
255
364
|
"""May this configuration name an executable inside the audited tree?
|
|
256
365
|
|
|
@@ -264,12 +373,15 @@ def target_executables_allowed(config: Config, target: Path) -> bool:
|
|
|
264
373
|
repositories wholesale, so a config the operator keeps outside the tree
|
|
265
374
|
still gets the documented tree-local interpreter workflow. Inside the tree,
|
|
266
375
|
it takes an explicit `--trust-target-config`.
|
|
376
|
+
|
|
377
|
+
See `target_config_is_untrusted` for why this is *not* expressed as its
|
|
378
|
+
negation: an absent config denies here and is harmless there.
|
|
267
379
|
"""
|
|
268
380
|
if config.trust_target_config:
|
|
269
381
|
return True
|
|
270
382
|
if config.source_path is None:
|
|
271
|
-
return False #
|
|
272
|
-
return not is_within(config.source_path, target)
|
|
383
|
+
return False # nothing authorised a tree-local command
|
|
384
|
+
return not is_within(config.source_path, containment_root(target))
|
|
273
385
|
|
|
274
386
|
|
|
275
387
|
#: Top-level keys the loader understands. Mirrors `properties` in
|
|
@@ -378,6 +490,19 @@ def _from_dict(raw: dict[str, Any]) -> Config:
|
|
|
378
490
|
outputs = raw.get("outputs", {})
|
|
379
491
|
if not isinstance(outputs, dict):
|
|
380
492
|
raise ValueError("outputs must be a JSON object")
|
|
493
|
+
# D2 applies inside `outputs`, and it did not. The loop below iterates
|
|
494
|
+
# the *known* keys, so anything else was read by nobody and reported by
|
|
495
|
+
# nobody: `outputs.markdwon_path` was accepted in silence while the
|
|
496
|
+
# report went to the default path. That is precisely the failure D2 cites
|
|
497
|
+
# for top-level keys — "a typo'd gate name disabled a gate with no
|
|
498
|
+
# diagnostic" — surviving one level down because the check was never
|
|
499
|
+
# applied recursively.
|
|
500
|
+
unknown = sorted(set(outputs) - set(DEFAULT_OUTPUTS))
|
|
501
|
+
if unknown:
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f"unknown outputs key(s): {', '.join(unknown)}. "
|
|
504
|
+
f"Known keys: {', '.join(sorted(DEFAULT_OUTPUTS))}"
|
|
505
|
+
)
|
|
381
506
|
for k, default_v in DEFAULT_OUTPUTS.items():
|
|
382
507
|
value = outputs.get(k, default_v)
|
|
383
508
|
# `null` turns an output off. The markdown report and the work order
|