secure-code-agent 0.4.0__tar.gz → 0.5.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.4.0/src/secure_code_agent.egg-info → secure_code_agent-0.5.0}/PKG-INFO +28 -1
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/README.md +27 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/pyproject.toml +15 -1
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0/src/secure_code_agent.egg-info}/PKG-INFO +28 -1
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/SOURCES.txt +2 -0
- secure_code_agent-0.5.0/src/secure_code_audit/__init__.py +15 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/cli.py +91 -13
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/config.py +36 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/git_tools.py +36 -6
- secure_code_agent-0.5.0/src/secure_code_audit/pillar.py +194 -0
- secure_code_agent-0.5.0/src/secure_code_audit/practice.py +300 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/renderers.py +93 -3
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scoring.py +121 -0
- secure_code_agent-0.4.0/src/secure_code_audit/__init__.py +0 -3
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/LICENSE +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/setup.cfg +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/requires.txt +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/baseline.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/findings.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/remediation.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/sarif.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanner_status.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/base.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/standards.py +0 -0
- {secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_audit/suppressions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secure-code-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -375,6 +375,31 @@ SARIF upload. Pin production usage to a full commit SHA; the version tag above
|
|
|
375
375
|
is shown for readability. See [`action.yml`](action.yml) and
|
|
376
376
|
[`examples/github-actions/`](examples/github-actions/) for full workflows.
|
|
377
377
|
|
|
378
|
+
## maintainability-agent integration
|
|
379
|
+
|
|
380
|
+
`maintainability-agent` declares Security a **delegated** pillar naming this
|
|
381
|
+
tool, and reports it as `NotApplicable` so silence is never read as safety.
|
|
382
|
+
This tool emits the artifact that completes the picture:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
secure-code-agent . --security-pillar security-pillar.json
|
|
386
|
+
maintainability-agent . --security-pillar security-pillar.json
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Standalone use is unaffected — without the flag nothing is written and every
|
|
390
|
+
other output is identical. MA never executes this tool, and this tool never
|
|
391
|
+
imports MA; two independently releasable packages exchanging one document.
|
|
392
|
+
|
|
393
|
+
The artifact carries **two values that are never averaged**: a practice level
|
|
394
|
+
read from configuration and CI (*is anything preventing the next
|
|
395
|
+
vulnerability?*) and a code condition read from the scanners (*what did they
|
|
396
|
+
find?*). `condition` is `null` whenever scanner coverage is incomplete — this
|
|
397
|
+
tool's score is a rate over findings, so removing scanners makes the raw number
|
|
398
|
+
go **up**, and an unscanned repository must not arrive at MA looking measured.
|
|
399
|
+
|
|
400
|
+
Full contract, invariants and the practice rubric:
|
|
401
|
+
[`docs/ma-integration.md`](docs/ma-integration.md).
|
|
402
|
+
|
|
378
403
|
## What this is NOT
|
|
379
404
|
|
|
380
405
|
- ❌ **Not a SAST engine.** We delegate to Semgrep / Bandit / CodeQL / etc. — we don't write yet another AST analyzer.
|
|
@@ -398,6 +423,8 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
398
423
|
|
|
399
424
|
- [`docs/product-intent.md`](docs/product-intent.md) — Why this exists, who it serves, what it refuses to become
|
|
400
425
|
- [`docs/decisions.md`](docs/decisions.md) — Decision register: rulings the code alone cannot answer
|
|
426
|
+
- [`docs/ma-integration.md`](docs/ma-integration.md) — The `security-pillar.json` contract maintainability-agent reads
|
|
427
|
+
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
401
428
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
402
429
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
403
430
|
- [`docs/release-blockers.md`](docs/release-blockers.md) — Open v0.3.0 release blockers (do not tag until closed)
|
|
@@ -329,6 +329,31 @@ SARIF upload. Pin production usage to a full commit SHA; the version tag above
|
|
|
329
329
|
is shown for readability. See [`action.yml`](action.yml) and
|
|
330
330
|
[`examples/github-actions/`](examples/github-actions/) for full workflows.
|
|
331
331
|
|
|
332
|
+
## maintainability-agent integration
|
|
333
|
+
|
|
334
|
+
`maintainability-agent` declares Security a **delegated** pillar naming this
|
|
335
|
+
tool, and reports it as `NotApplicable` so silence is never read as safety.
|
|
336
|
+
This tool emits the artifact that completes the picture:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
secure-code-agent . --security-pillar security-pillar.json
|
|
340
|
+
maintainability-agent . --security-pillar security-pillar.json
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Standalone use is unaffected — without the flag nothing is written and every
|
|
344
|
+
other output is identical. MA never executes this tool, and this tool never
|
|
345
|
+
imports MA; two independently releasable packages exchanging one document.
|
|
346
|
+
|
|
347
|
+
The artifact carries **two values that are never averaged**: a practice level
|
|
348
|
+
read from configuration and CI (*is anything preventing the next
|
|
349
|
+
vulnerability?*) and a code condition read from the scanners (*what did they
|
|
350
|
+
find?*). `condition` is `null` whenever scanner coverage is incomplete — this
|
|
351
|
+
tool's score is a rate over findings, so removing scanners makes the raw number
|
|
352
|
+
go **up**, and an unscanned repository must not arrive at MA looking measured.
|
|
353
|
+
|
|
354
|
+
Full contract, invariants and the practice rubric:
|
|
355
|
+
[`docs/ma-integration.md`](docs/ma-integration.md).
|
|
356
|
+
|
|
332
357
|
## What this is NOT
|
|
333
358
|
|
|
334
359
|
- ❌ **Not a SAST engine.** We delegate to Semgrep / Bandit / CodeQL / etc. — we don't write yet another AST analyzer.
|
|
@@ -352,6 +377,8 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
352
377
|
|
|
353
378
|
- [`docs/product-intent.md`](docs/product-intent.md) — Why this exists, who it serves, what it refuses to become
|
|
354
379
|
- [`docs/decisions.md`](docs/decisions.md) — Decision register: rulings the code alone cannot answer
|
|
380
|
+
- [`docs/ma-integration.md`](docs/ma-integration.md) — The `security-pillar.json` contract maintainability-agent reads
|
|
381
|
+
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
355
382
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
356
383
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
357
384
|
- [`docs/release-blockers.md`](docs/release-blockers.md) — Open v0.3.0 release blockers (do not tag until closed)
|
|
@@ -4,7 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "secure-code-agent"
|
|
7
|
-
|
|
7
|
+
# Derived from `secure_code_audit.__version__`, which is the only place the
|
|
8
|
+
# number is written. It used to be duplicated here, and the two drifted: this
|
|
9
|
+
# file said 0.4.0 while the package said 0.3.0, so the released v0.4.0 wheel
|
|
10
|
+
# stamped 0.3.0 into every SARIF document, JSON report, Markdown header,
|
|
11
|
+
# `--version` and the pillar artifact handed to maintainability-agent. The
|
|
12
|
+
# release workflow compared the tag against *this* line and never looked at the
|
|
13
|
+
# package, so nothing caught it. A test asserting the two agree would only
|
|
14
|
+
# police the duplication; removing it is the fix.
|
|
15
|
+
dynamic = ["version"]
|
|
8
16
|
description = "Deterministic security gate + bounded AI remediation prompt generator. NIST SSDF / OWASP ASVS / CWE Top 25 anchored."
|
|
9
17
|
readme = "README.md"
|
|
10
18
|
requires-python = ">=3.10"
|
|
@@ -86,6 +94,12 @@ Documentation = "https://github.com/marshallguillory86/secure-code-agent/tree/ma
|
|
|
86
94
|
Issues = "https://github.com/marshallguillory86/secure-code-agent/issues"
|
|
87
95
|
Changelog = "https://github.com/marshallguillory86/secure-code-agent/blob/main/CHANGELOG.md"
|
|
88
96
|
|
|
97
|
+
[tool.setuptools.dynamic]
|
|
98
|
+
# The single source of truth for the version. `src/secure_code_audit/__init__.py`
|
|
99
|
+
# holds the literal; this reads it. The two cannot drift because there is only
|
|
100
|
+
# one of them.
|
|
101
|
+
version = { attr = "secure_code_audit.__version__" }
|
|
102
|
+
|
|
89
103
|
[tool.setuptools.packages.find]
|
|
90
104
|
where = ["src"]
|
|
91
105
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: secure-code-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.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
|
|
@@ -375,6 +375,31 @@ SARIF upload. Pin production usage to a full commit SHA; the version tag above
|
|
|
375
375
|
is shown for readability. See [`action.yml`](action.yml) and
|
|
376
376
|
[`examples/github-actions/`](examples/github-actions/) for full workflows.
|
|
377
377
|
|
|
378
|
+
## maintainability-agent integration
|
|
379
|
+
|
|
380
|
+
`maintainability-agent` declares Security a **delegated** pillar naming this
|
|
381
|
+
tool, and reports it as `NotApplicable` so silence is never read as safety.
|
|
382
|
+
This tool emits the artifact that completes the picture:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
secure-code-agent . --security-pillar security-pillar.json
|
|
386
|
+
maintainability-agent . --security-pillar security-pillar.json
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Standalone use is unaffected — without the flag nothing is written and every
|
|
390
|
+
other output is identical. MA never executes this tool, and this tool never
|
|
391
|
+
imports MA; two independently releasable packages exchanging one document.
|
|
392
|
+
|
|
393
|
+
The artifact carries **two values that are never averaged**: a practice level
|
|
394
|
+
read from configuration and CI (*is anything preventing the next
|
|
395
|
+
vulnerability?*) and a code condition read from the scanners (*what did they
|
|
396
|
+
find?*). `condition` is `null` whenever scanner coverage is incomplete — this
|
|
397
|
+
tool's score is a rate over findings, so removing scanners makes the raw number
|
|
398
|
+
go **up**, and an unscanned repository must not arrive at MA looking measured.
|
|
399
|
+
|
|
400
|
+
Full contract, invariants and the practice rubric:
|
|
401
|
+
[`docs/ma-integration.md`](docs/ma-integration.md).
|
|
402
|
+
|
|
378
403
|
## What this is NOT
|
|
379
404
|
|
|
380
405
|
- ❌ **Not a SAST engine.** We delegate to Semgrep / Bandit / CodeQL / etc. — we don't write yet another AST analyzer.
|
|
@@ -398,6 +423,8 @@ Full design philosophy in [`docs/design.md`](docs/design.md).
|
|
|
398
423
|
|
|
399
424
|
- [`docs/product-intent.md`](docs/product-intent.md) — Why this exists, who it serves, what it refuses to become
|
|
400
425
|
- [`docs/decisions.md`](docs/decisions.md) — Decision register: rulings the code alone cannot answer
|
|
426
|
+
- [`docs/ma-integration.md`](docs/ma-integration.md) — The `security-pillar.json` contract maintainability-agent reads
|
|
427
|
+
- [`docs/calibration.md`](docs/calibration.md) — The calibration study, its corpus, and what it found
|
|
401
428
|
- [`docs/design.md`](docs/design.md) — Architecture + non-goals + scanner protocol
|
|
402
429
|
- [`docs/architecture.md`](docs/architecture.md) — Audit of the system as built + remediation sequence
|
|
403
430
|
- [`docs/release-blockers.md`](docs/release-blockers.md) — Open v0.3.0 release blockers (do not tag until closed)
|
{secure_code_agent-0.4.0 → secure_code_agent-0.5.0}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
@@ -14,6 +14,8 @@ src/secure_code_audit/config.py
|
|
|
14
14
|
src/secure_code_audit/findings.py
|
|
15
15
|
src/secure_code_audit/git_tools.py
|
|
16
16
|
src/secure_code_audit/instructions.py
|
|
17
|
+
src/secure_code_audit/pillar.py
|
|
18
|
+
src/secure_code_audit/practice.py
|
|
17
19
|
src/secure_code_audit/remediation.py
|
|
18
20
|
src/secure_code_audit/renderers.py
|
|
19
21
|
src/secure_code_audit/ruleset.py
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""secure-code-agent — deterministic security gate + bounded AI remediation prompts."""
|
|
2
|
+
|
|
3
|
+
#: The only place the version is written. `pyproject.toml` derives it via
|
|
4
|
+
#: `[tool.setuptools.dynamic]`, so the two cannot drift.
|
|
5
|
+
#:
|
|
6
|
+
#: They did drift once, and it shipped: pyproject said 0.4.0 while this said
|
|
7
|
+
#: 0.3.0, so the released v0.4.0 wheel stamped 0.3.0 into every SARIF document,
|
|
8
|
+
#: every JSON report, the Markdown header, `--version`, and the
|
|
9
|
+
#: `security-pillar.json` handed to maintainability-agent. The release workflow
|
|
10
|
+
#: compared the tag against pyproject's line and never looked here — it
|
|
11
|
+
#: verified the half that was right and shipped the half that was wrong.
|
|
12
|
+
#:
|
|
13
|
+
#: PyPI is immutable, so 0.4.0 stays wrong. 0.5.0 is the first build whose
|
|
14
|
+
#: artifacts name their own producer correctly.
|
|
15
|
+
__version__ = "0.5.0"
|
|
@@ -24,8 +24,10 @@ from secure_code_audit import (
|
|
|
24
24
|
)
|
|
25
25
|
from secure_code_audit import baseline as baseline_mod
|
|
26
26
|
from secure_code_audit import config as config_mod
|
|
27
|
+
from secure_code_audit import pillar as pillar_mod
|
|
28
|
+
from secure_code_audit import practice as practice_mod
|
|
27
29
|
from secure_code_audit.findings import Category, Finding, Severity
|
|
28
|
-
from secure_code_audit.git_tools import find_repo_root, is_excluded, loc_under
|
|
30
|
+
from secure_code_audit.git_tools import find_repo_root, is_excluded, is_test_path, loc_under
|
|
29
31
|
from secure_code_audit.scanner_status import (
|
|
30
32
|
ScannerExecution,
|
|
31
33
|
ScannerOutcome,
|
|
@@ -33,7 +35,13 @@ from secure_code_audit.scanner_status import (
|
|
|
33
35
|
execution_from_result,
|
|
34
36
|
)
|
|
35
37
|
from secure_code_audit.scanners import floor
|
|
36
|
-
from secure_code_audit.scoring import
|
|
38
|
+
from secure_code_audit.scoring import (
|
|
39
|
+
active_gates,
|
|
40
|
+
evaluate_gates,
|
|
41
|
+
partition_by_tree,
|
|
42
|
+
split_side_axes,
|
|
43
|
+
summarize_axis,
|
|
44
|
+
)
|
|
37
45
|
from secure_code_audit.scoring import score as score_findings
|
|
38
46
|
from secure_code_audit.scoring import verdict as build_verdict
|
|
39
47
|
|
|
@@ -60,6 +68,14 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
60
68
|
p.add_argument("--sarif-output", help="SARIF 2.1.0 output path.")
|
|
61
69
|
p.add_argument("--comment-output", help="PR-comment markdown output path.")
|
|
62
70
|
p.add_argument("--prompt-output", help="Remediation prompt output path.")
|
|
71
|
+
p.add_argument(
|
|
72
|
+
"--security-pillar",
|
|
73
|
+
help=(
|
|
74
|
+
"Write security-pillar.json for maintainability-agent to ingest "
|
|
75
|
+
"via --security-pillar (D3). Carries practice level and code "
|
|
76
|
+
"condition as two values that are never averaged."
|
|
77
|
+
),
|
|
78
|
+
)
|
|
63
79
|
p.add_argument("--baseline", help="Baseline file path (read).")
|
|
64
80
|
p.add_argument(
|
|
65
81
|
"--bump-baseline", action="store_true", help="Rewrite the baseline from current findings."
|
|
@@ -290,11 +306,37 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
290
306
|
all_findings = baseline_mod.mark_new(all_findings, baseline)
|
|
291
307
|
|
|
292
308
|
# ----- scoring -----
|
|
309
|
+
# The test tree is reported, not scored. A project graded on its test
|
|
310
|
+
# fixtures is graded on the wrong thing: across the calibration corpus,
|
|
311
|
+
# including test directories moved the median normalized subtotal from
|
|
312
|
+
# 4.36 to 50.15 and put ten of fourteen well-maintained projects at F.
|
|
313
|
+
# Secrets are exempt and stay in the score — see ALWAYS_SCORED_CATEGORIES.
|
|
314
|
+
root_for_tests = target if target.is_dir() else target.parent
|
|
315
|
+
all_findings, test_findings = partition_by_tree(
|
|
316
|
+
all_findings,
|
|
317
|
+
lambda f: is_test_path(f.file_path, root_for_tests, cfg.test_patterns),
|
|
318
|
+
)
|
|
293
319
|
if cfg.loc_for_scoring:
|
|
294
320
|
loc = int(cfg.loc_for_scoring.get("value", 0))
|
|
321
|
+
test_loc = 0
|
|
295
322
|
else:
|
|
296
|
-
loc = loc_under(
|
|
297
|
-
|
|
323
|
+
loc, test_loc = loc_under(
|
|
324
|
+
target, cfg.include_extensions, cfg.exclude_patterns, cfg.test_patterns
|
|
325
|
+
)
|
|
326
|
+
# Dependencies come off the code-condition score and onto their own axis.
|
|
327
|
+
# A CVE in a pinned dependency is fixed with a version bump; an injection
|
|
328
|
+
# flaw is fixed with a rewrite. Averaging them produced the largest
|
|
329
|
+
# remaining distortion in the corpus — median 15.36 against 7.32.
|
|
330
|
+
#
|
|
331
|
+
# The split is between *scoring* and *gating*, not between reported and
|
|
332
|
+
# hidden: `gated_findings` keeps the dependency advisories, so a critical
|
|
333
|
+
# runtime CVE still fails a build exactly as before.
|
|
334
|
+
gated_findings = all_findings
|
|
335
|
+
scored_findings, dependency_findings = split_side_axes(all_findings)
|
|
336
|
+
score = score_findings(scored_findings, loc)
|
|
337
|
+
test_tree = summarize_axis("test tree", test_findings, test_loc)
|
|
338
|
+
dependencies = summarize_axis("dependencies", dependency_findings)
|
|
339
|
+
axes = (test_tree, dependencies)
|
|
298
340
|
# Naming an import on the command line asserts that it contributes coverage,
|
|
299
341
|
# so a broken one fails the gate even if no config requires that scanner.
|
|
300
342
|
# Requiring a tool that has nothing to look at would make every
|
|
@@ -311,23 +353,41 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
311
353
|
*(execution.name for execution in imported_executions),
|
|
312
354
|
]
|
|
313
355
|
coverage = evaluate_coverage(executions, required)
|
|
314
|
-
|
|
356
|
+
# Gates see the dependency advisories; the score does not.
|
|
357
|
+
gate = evaluate_gates(gated_findings, score, cfg.gates, coverage)
|
|
315
358
|
verdict = build_verdict(score, cfg.gates, coverage)
|
|
316
359
|
|
|
317
360
|
# ----- write outputs -----
|
|
318
361
|
paths = _resolve_outputs(args, cfg, root)
|
|
319
|
-
|
|
362
|
+
# The pillar artifact is what maintainability-agent ingests (D3). Built
|
|
363
|
+
# here rather than inside a renderer because it needs the practice level,
|
|
364
|
+
# which is read from configuration and CI rather than from findings.
|
|
365
|
+
security_pillar = pillar_mod.build(score, verdict, coverage, practice_mod.assess(target), axes)
|
|
366
|
+
_write_outputs(
|
|
367
|
+
paths,
|
|
368
|
+
all_findings,
|
|
369
|
+
score,
|
|
370
|
+
gate,
|
|
371
|
+
coverage,
|
|
372
|
+
ran,
|
|
373
|
+
unavailable,
|
|
374
|
+
verdict,
|
|
375
|
+
axes,
|
|
376
|
+
security_pillar,
|
|
377
|
+
)
|
|
320
378
|
if args.bump_baseline:
|
|
321
379
|
baseline_mod.write(baseline_path, all_findings, baseline)
|
|
322
380
|
|
|
323
381
|
# ----- terminal output -----
|
|
324
382
|
if args.json:
|
|
325
383
|
sys.stdout.write(
|
|
326
|
-
json.dumps(
|
|
384
|
+
json.dumps(
|
|
385
|
+
renderers.to_json(all_findings, score, gate, coverage, verdict, axes), indent=2
|
|
386
|
+
)
|
|
327
387
|
)
|
|
328
388
|
sys.stdout.write("\n")
|
|
329
389
|
else:
|
|
330
|
-
_print_summary(verdict, score, gate, ran, unavailable, coverage, paths)
|
|
390
|
+
_print_summary(verdict, score, gate, ran, unavailable, coverage, paths, axes)
|
|
331
391
|
|
|
332
392
|
return _exit_code(args, gate, all_findings)
|
|
333
393
|
|
|
@@ -523,19 +583,25 @@ def _ingest_sarif_imports(specs: list[str]) -> tuple[list[Finding], list[Scanner
|
|
|
523
583
|
return findings, executions
|
|
524
584
|
|
|
525
585
|
|
|
526
|
-
def _write_outputs(
|
|
586
|
+
def _write_outputs(
|
|
587
|
+
paths, findings, score, gate, coverage, ran, unavailable, verdict, axes=(), pillar=None
|
|
588
|
+
) -> None:
|
|
527
589
|
if paths.markdown is not None:
|
|
528
590
|
renderers.write_markdown(
|
|
529
|
-
findings, score, gate, paths.markdown, ran, unavailable, coverage, verdict
|
|
591
|
+
findings, score, gate, paths.markdown, ran, unavailable, coverage, verdict, axes
|
|
530
592
|
)
|
|
531
593
|
if paths.json_out is not None:
|
|
532
|
-
renderers.write_json(findings, score, gate, paths.json_out, coverage, verdict)
|
|
594
|
+
renderers.write_json(findings, score, gate, paths.json_out, coverage, verdict, axes)
|
|
533
595
|
if paths.sarif is not None:
|
|
534
596
|
sarif.write(findings, paths.sarif, coverage)
|
|
535
597
|
if paths.comment is not None:
|
|
536
598
|
renderers.write_pr_comment(findings, score, gate, paths.comment, coverage, verdict)
|
|
537
599
|
if paths.prompt is not None:
|
|
538
600
|
remediation.write(findings, paths.prompt)
|
|
601
|
+
# The artifact maintainability-agent ingests (D3). Written last because it
|
|
602
|
+
# is the only output that carries both axes plus the practice level.
|
|
603
|
+
if paths.security_pillar is not None and pillar is not None:
|
|
604
|
+
pillar_mod.write(pillar, paths.security_pillar)
|
|
539
605
|
|
|
540
606
|
|
|
541
607
|
def _apply_overrides(findings: list[Finding], cfg: config_mod.Config) -> list[Finding]:
|
|
@@ -562,6 +628,7 @@ class _OutputPaths:
|
|
|
562
628
|
sarif: Path | None
|
|
563
629
|
comment: Path | None
|
|
564
630
|
prompt: Path | None
|
|
631
|
+
security_pillar: Path | None
|
|
565
632
|
|
|
566
633
|
|
|
567
634
|
def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Path) -> _OutputPaths:
|
|
@@ -580,6 +647,7 @@ def _resolve_outputs(args: argparse.Namespace, cfg: config_mod.Config, root: Pat
|
|
|
580
647
|
sarif=_p(args.sarif_output),
|
|
581
648
|
comment=_p(args.comment_output),
|
|
582
649
|
prompt=_p(args.prompt_output),
|
|
650
|
+
security_pillar=_p(args.security_pillar),
|
|
583
651
|
)
|
|
584
652
|
|
|
585
653
|
|
|
@@ -588,12 +656,15 @@ def _under_root(root: Path, value: str) -> Path:
|
|
|
588
656
|
return path.resolve() if path.is_absolute() else (root / path).resolve()
|
|
589
657
|
|
|
590
658
|
|
|
591
|
-
def _print_summary(verdict, score, gate, ran, unavailable, coverage, paths) -> None:
|
|
659
|
+
def _print_summary(verdict, score, gate, ran, unavailable, coverage, paths, axes=()) -> None:
|
|
592
660
|
status = "PASS" if gate.passed else "FAIL"
|
|
593
661
|
print(f"secure-code-agent · score {verdict.headline()} · gate {status}")
|
|
594
662
|
for reason in verdict.reasons:
|
|
595
663
|
print(f" ! grade withheld: {reason}")
|
|
596
664
|
print(f" scanned LOC: {score.loc_scanned:,}")
|
|
665
|
+
for axis in axes or ():
|
|
666
|
+
if axis.count or axis.loc:
|
|
667
|
+
print(f" {axis.headline()}")
|
|
597
668
|
print(f" scanners run: {', '.join(ran) if ran else '(none)'}")
|
|
598
669
|
coverage_line = f" coverage: {coverage.status.value.upper()}"
|
|
599
670
|
if coverage.unverified:
|
|
@@ -608,7 +679,14 @@ def _print_summary(verdict, score, gate, ran, unavailable, coverage, paths) -> N
|
|
|
608
679
|
print(f" ✗ {reason}")
|
|
609
680
|
written = [
|
|
610
681
|
str(p)
|
|
611
|
-
for p in (
|
|
682
|
+
for p in (
|
|
683
|
+
paths.markdown,
|
|
684
|
+
paths.json_out,
|
|
685
|
+
paths.sarif,
|
|
686
|
+
paths.comment,
|
|
687
|
+
paths.prompt,
|
|
688
|
+
paths.security_pillar,
|
|
689
|
+
)
|
|
612
690
|
if p is not None
|
|
613
691
|
]
|
|
614
692
|
if written:
|
|
@@ -28,6 +28,33 @@ DEFAULT_EXCLUDES: tuple[str, ...] = (
|
|
|
28
28
|
"**/*.lock",
|
|
29
29
|
)
|
|
30
30
|
|
|
31
|
+
#: Conventional test-tree locations across the languages the floor reads.
|
|
32
|
+
#: Deliberately conservative: a false positive here moves real findings out of
|
|
33
|
+
#: the score, so `src/` layouts and single `*_test.go` files are matched by
|
|
34
|
+
#: name rather than by guessing at directory intent.
|
|
35
|
+
DEFAULT_TEST_PATTERNS: tuple[str, ...] = (
|
|
36
|
+
"test/",
|
|
37
|
+
"tests/",
|
|
38
|
+
"spec/",
|
|
39
|
+
"specs/",
|
|
40
|
+
"testing/",
|
|
41
|
+
"__tests__/",
|
|
42
|
+
"**/test/",
|
|
43
|
+
"**/tests/",
|
|
44
|
+
"**/spec/",
|
|
45
|
+
"**/__tests__/",
|
|
46
|
+
"**/test_*.py",
|
|
47
|
+
"**/*_test.py",
|
|
48
|
+
"**/*_test.go",
|
|
49
|
+
"**/*_test.rb",
|
|
50
|
+
"**/*.test.js",
|
|
51
|
+
"**/*.spec.js",
|
|
52
|
+
"**/*.test.ts",
|
|
53
|
+
"**/*.spec.ts",
|
|
54
|
+
"**/*Test.java",
|
|
55
|
+
"**/*Tests.java",
|
|
56
|
+
)
|
|
57
|
+
|
|
31
58
|
DEFAULT_INCLUDE_EXTS: tuple[str, ...] = (
|
|
32
59
|
".py",
|
|
33
60
|
".js",
|
|
@@ -89,6 +116,13 @@ class Config:
|
|
|
89
116
|
version: int = 1
|
|
90
117
|
include_extensions: tuple[str, ...] = DEFAULT_INCLUDE_EXTS
|
|
91
118
|
exclude_patterns: tuple[str, ...] = DEFAULT_EXCLUDES
|
|
119
|
+
#: Paths that are the repository's *own* test tree. Findings there are
|
|
120
|
+
#: reported separately rather than scored, because a project graded on its
|
|
121
|
+
#: test fixtures is graded on the wrong thing — measured at 50.15 vs 4.36
|
|
122
|
+
#: median normalized subtotal, see docs/calibration.md. Not an exclusion:
|
|
123
|
+
#: the findings are still collected, still reported, and secrets among them
|
|
124
|
+
#: still score and still gate.
|
|
125
|
+
test_patterns: tuple[str, ...] = DEFAULT_TEST_PATTERNS
|
|
92
126
|
scanners: dict[str, ScannerConfig] = field(default_factory=dict)
|
|
93
127
|
severity_overrides: dict[str, str] = field(default_factory=dict)
|
|
94
128
|
category_overrides: dict[str, str] = field(default_factory=dict)
|
|
@@ -216,6 +250,8 @@ def _from_dict(raw: dict[str, Any]) -> Config:
|
|
|
216
250
|
cfg.exclude_patterns = tuple(
|
|
217
251
|
_string_list(paths["exclude_patterns"], "paths.exclude_patterns")
|
|
218
252
|
)
|
|
253
|
+
if "test_patterns" in paths:
|
|
254
|
+
cfg.test_patterns = tuple(_string_list(paths["test_patterns"], "paths.test_patterns"))
|
|
219
255
|
|
|
220
256
|
scanners_raw = raw.get("scanners", {})
|
|
221
257
|
if not isinstance(scanners_raw, dict):
|
|
@@ -46,10 +46,36 @@ def in_scope(path: Path, include_exts: Iterable[str]) -> bool:
|
|
|
46
46
|
return False
|
|
47
47
|
|
|
48
48
|
|
|
49
|
-
def
|
|
50
|
-
"""
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
def is_test_path(path: Path, root: Path, patterns: Iterable[str]) -> bool:
|
|
50
|
+
"""Does this path belong to the repository's own test tree?
|
|
51
|
+
|
|
52
|
+
Same matching as `is_excluded`, and deliberately so — an operator who can
|
|
53
|
+
write an exclude pattern already knows how to write one of these.
|
|
54
|
+
|
|
55
|
+
A path outside `root` is not a test path. An imported SARIF can name
|
|
56
|
+
absolute paths from another machine, and guessing that someone else's
|
|
57
|
+
`/build/tests/` is our test tree would move real findings out of the score.
|
|
58
|
+
"""
|
|
59
|
+
return is_excluded(path, root, patterns)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def loc_under(
|
|
63
|
+
root: Path,
|
|
64
|
+
include_exts: Iterable[str],
|
|
65
|
+
excludes: Iterable[str],
|
|
66
|
+
test_patterns: Iterable[str] = (),
|
|
67
|
+
) -> tuple[int, int]:
|
|
68
|
+
"""Non-blank in-scope lines, split into (primary, test).
|
|
69
|
+
|
|
70
|
+
The split exists because the score's denominator has to move with its
|
|
71
|
+
numerator. Scoring primary-tree findings over a LOC count that included the
|
|
72
|
+
test tree would understate every repository in proportion to how well it is
|
|
73
|
+
tested — the same numerator/denominator mismatch that `exclude_patterns`
|
|
74
|
+
already caused once, arriving by a different door.
|
|
75
|
+
"""
|
|
76
|
+
test_patterns = tuple(test_patterns)
|
|
77
|
+
primary = 0
|
|
78
|
+
test = 0
|
|
53
79
|
for path in root.rglob("*"):
|
|
54
80
|
if not path.is_file():
|
|
55
81
|
continue
|
|
@@ -61,5 +87,9 @@ def loc_under(root: Path, include_exts: Iterable[str], excludes: Iterable[str])
|
|
|
61
87
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
62
88
|
except OSError:
|
|
63
89
|
continue
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
lines = sum(1 for line in text.splitlines() if line.strip())
|
|
91
|
+
if test_patterns and is_test_path(path, root, test_patterns):
|
|
92
|
+
test += lines
|
|
93
|
+
else:
|
|
94
|
+
primary += lines
|
|
95
|
+
return primary, test
|