secure-code-agent 0.8.0__tar.gz → 0.9.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.8.0/src/secure_code_agent.egg-info → secure_code_agent-0.9.0}/PKG-INFO +1 -1
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0/src/secure_code_agent.egg-info}/PKG-INFO +1 -1
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/__init__.py +1 -1
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/baseline.py +31 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/cli.py +11 -3
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/config.py +32 -2
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/git_tools.py +14 -3
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scoring.py +20 -1
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/LICENSE +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/README.md +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/pyproject.toml +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/setup.cfg +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/SOURCES.txt +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/dependency_links.txt +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/entry_points.txt +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/requires.txt +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/top_level.txt +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/data/semgrep-offline.yaml +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/findings.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/history.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/instructions.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/pillar.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/practice.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/remediation.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/renderers.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/ruleset.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/sarif.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanner_status.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/__init__.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/bandit_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/base.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/builtin_rules.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/checkov_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/floor.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/gitleaks_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/gosec_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/hadolint_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/njsscan_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/npm_audit_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/osv_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/pip_audit_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/rubocop_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/scorecard_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/semgrep_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/trivy_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/trufflehog_scanner.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/standards.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/suppressions.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/triage.py +0 -0
- {secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/verify.py +0 -0
|
@@ -10,6 +10,7 @@ operator has acknowledged at a point in time. On the next run:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import datetime
|
|
13
|
+
import enum
|
|
13
14
|
import json
|
|
14
15
|
import shutil
|
|
15
16
|
import subprocess
|
|
@@ -32,6 +33,36 @@ class BaselineEntry:
|
|
|
32
33
|
notes: str = ""
|
|
33
34
|
|
|
34
35
|
|
|
36
|
+
class State(enum.Enum):
|
|
37
|
+
"""Why the baseline is the shape it is.
|
|
38
|
+
|
|
39
|
+
`load` returns an empty mapping for a baseline that is absent, one that
|
|
40
|
+
is unreadable, and one that is genuinely empty. Downstream those look
|
|
41
|
+
identical and every finding reads as new — which is correct for the
|
|
42
|
+
first case, a silent failure for the second, and worth saying out loud
|
|
43
|
+
in all three once `fail_on_new` gates by default.
|
|
44
|
+
|
|
45
|
+
On a first run the honest message is "there is nothing to compare
|
|
46
|
+
against yet", not "2 new findings since baseline", which claims a
|
|
47
|
+
baseline exists and that these appeared after it.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
ABSENT = "absent"
|
|
51
|
+
UNREADABLE = "unreadable"
|
|
52
|
+
PRESENT = "present"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def state(path: Path) -> State:
|
|
56
|
+
"""Distinguish a missing baseline from a broken one."""
|
|
57
|
+
if not path.exists():
|
|
58
|
+
return State.ABSENT
|
|
59
|
+
try:
|
|
60
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
61
|
+
except (OSError, json.JSONDecodeError):
|
|
62
|
+
return State.UNREADABLE
|
|
63
|
+
return State.PRESENT if isinstance(raw, dict) else State.UNREADABLE
|
|
64
|
+
|
|
65
|
+
|
|
35
66
|
def load(path: Path) -> dict[str, BaselineEntry]:
|
|
36
67
|
if not path.exists():
|
|
37
68
|
return {}
|
|
@@ -338,6 +338,7 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
338
338
|
# ----- baseline -----
|
|
339
339
|
baseline_path = _under_root(root, args.baseline or cfg.outputs["baseline_path"])
|
|
340
340
|
baseline = baseline_mod.load(baseline_path)
|
|
341
|
+
baseline_state = baseline_mod.state(baseline_path)
|
|
341
342
|
all_findings = baseline_mod.mark_new(all_findings, baseline)
|
|
342
343
|
|
|
343
344
|
# ----- scoring -----
|
|
@@ -377,8 +378,9 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
377
378
|
if cfg.loc_for_scoring:
|
|
378
379
|
loc = int(cfg.loc_for_scoring.get("value", 0))
|
|
379
380
|
test_loc = 0
|
|
381
|
+
docs_loc = 0
|
|
380
382
|
else:
|
|
381
|
-
loc, test_loc = loc_under(
|
|
383
|
+
loc, test_loc, docs_loc = loc_under(
|
|
382
384
|
target,
|
|
383
385
|
cfg.include_extensions,
|
|
384
386
|
cfg.exclude_patterns,
|
|
@@ -386,6 +388,7 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
386
388
|
# Same set the findings were filtered against. Numerator and
|
|
387
389
|
# denominator have to describe the same repository.
|
|
388
390
|
own_artifacts,
|
|
391
|
+
cfg.docs_patterns,
|
|
389
392
|
)
|
|
390
393
|
# Dependencies come off the code-condition score and onto their own axis.
|
|
391
394
|
# A CVE in a pinned dependency is fixed with a version bump; an injection
|
|
@@ -405,7 +408,7 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
405
408
|
score = score_findings(scored_findings, loc, measurable)
|
|
406
409
|
axes = (
|
|
407
410
|
summarize_axis("test tree", test_findings, test_loc),
|
|
408
|
-
summarize_axis("documentation", docs_findings),
|
|
411
|
+
summarize_axis("documentation", docs_findings, docs_loc or None),
|
|
409
412
|
summarize_axis("dependencies", dependency_findings),
|
|
410
413
|
)
|
|
411
414
|
# Naming an import on the command line asserts that it contributes coverage,
|
|
@@ -425,7 +428,12 @@ def _do_audit(args: argparse.Namespace) -> int:
|
|
|
425
428
|
]
|
|
426
429
|
coverage = evaluate_coverage(executions, required)
|
|
427
430
|
# Gates see the dependency advisories; the score does not.
|
|
428
|
-
gate
|
|
431
|
+
# The gate needs to know *why* the baseline is empty to describe a first
|
|
432
|
+
# run truthfully. Passed in the config dict rather than as a parameter so
|
|
433
|
+
# the gate signature stays the one every check shares.
|
|
434
|
+
gate = evaluate_gates(
|
|
435
|
+
gated, score, {**cfg.gates, "_baseline_state": baseline_state.value}, coverage
|
|
436
|
+
)
|
|
429
437
|
verdict = build_verdict(score, cfg.gates, coverage)
|
|
430
438
|
|
|
431
439
|
# ----- write outputs -----
|
|
@@ -26,6 +26,17 @@ DEFAULT_EXCLUDES: tuple[str, ...] = (
|
|
|
26
26
|
".mypy_cache/",
|
|
27
27
|
"**/*.min.js",
|
|
28
28
|
"**/*.lock",
|
|
29
|
+
# Lockfiles that are not named `.lock`. `**/*.lock` catches
|
|
30
|
+
# `poetry.lock`, `Gemfile.lock`, `Cargo.lock` and `yarn.lock` and misses
|
|
31
|
+
# every lockfile the JavaScript ecosystem actually ships:
|
|
32
|
+
# `package-lock.json` alone was 9,699 of axios's 17,532 non-code lines
|
|
33
|
+
# and 5,845 of lodash's 6,222. A generated dependency manifest is not
|
|
34
|
+
# source, and counting it inflates the denominator that decides the
|
|
35
|
+
# grade.
|
|
36
|
+
"**/package-lock.json",
|
|
37
|
+
"**/npm-shrinkwrap.json",
|
|
38
|
+
"**/pnpm-lock.yaml",
|
|
39
|
+
"**/bun.lockb",
|
|
29
40
|
)
|
|
30
41
|
|
|
31
42
|
#: Conventional test-tree locations across the languages the floor reads.
|
|
@@ -169,7 +180,24 @@ class Config:
|
|
|
169
180
|
scanners: dict[str, ScannerConfig] = field(default_factory=dict)
|
|
170
181
|
severity_overrides: dict[str, str] = field(default_factory=dict)
|
|
171
182
|
category_overrides: dict[str, str] = field(default_factory=dict)
|
|
172
|
-
|
|
183
|
+
#: Default policy: **ratchet on regressions**, not on absolute state.
|
|
184
|
+
#:
|
|
185
|
+
#: `fail_on_new` is the one gate measured to work. Severity-based
|
|
186
|
+
#: defaults cannot: `fail_on_severity: ["critical"]` caught none of four
|
|
187
|
+
#: known-vulnerable control repositories, and `["critical","high"]`
|
|
188
|
+
#: failed five of ten well-maintained ones while still missing SQL
|
|
189
|
+
#: injection and `pickle.loads`, which Bandit rates *medium*. See D15.
|
|
190
|
+
#:
|
|
191
|
+
#: The ratchet reads no severity at all, so it inherits none of that. A
|
|
192
|
+
#: false positive is baselined once and never asked about again, which
|
|
193
|
+
#: is the property a threshold cannot have. Measured across an adoption
|
|
194
|
+
#: lifecycle: first run fails (everything is new), `--bump-baseline`
|
|
195
|
+
#: accepts existing debt, an introduced SQL injection fails, reverting
|
|
196
|
+
#: passes.
|
|
197
|
+
#:
|
|
198
|
+
#: An empty `{}` was the previous default and provided no floor at all —
|
|
199
|
+
#: an absent gate cannot trip, so every audit "passed".
|
|
200
|
+
gates: dict[str, Any] = field(default_factory=lambda: {"fail_on_new": True})
|
|
173
201
|
outputs: dict[str, str] = field(default_factory=lambda: dict(DEFAULT_OUTPUTS))
|
|
174
202
|
suppressions_file: str = ".scignore.yaml"
|
|
175
203
|
loc_for_scoring: dict[str, Any] | None = None
|
|
@@ -343,7 +371,9 @@ def _from_dict(raw: dict[str, Any]) -> Config:
|
|
|
343
371
|
raise ValueError(f"invalid severity override: {', '.join(invalid)}")
|
|
344
372
|
if invalid := sorted(set(cfg.category_overrides.values()) - _CATEGORIES):
|
|
345
373
|
raise ValueError(f"invalid category override: {', '.join(invalid)}")
|
|
346
|
-
|
|
374
|
+
# An operator who writes a `gates` block chooses their own policy
|
|
375
|
+
# entirely; the ratchet default applies only when they write none.
|
|
376
|
+
cfg.gates = _validate_gates(raw["gates"]) if "gates" in raw else dict(cfg.gates)
|
|
347
377
|
|
|
348
378
|
outputs = raw.get("outputs", {})
|
|
349
379
|
if not isinstance(outputs, dict):
|
|
@@ -97,8 +97,9 @@ def loc_under(
|
|
|
97
97
|
excludes: Iterable[str],
|
|
98
98
|
test_patterns: Iterable[str] = (),
|
|
99
99
|
skip: Iterable[Path] = (),
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
docs_patterns: Iterable[str] = (),
|
|
101
|
+
) -> tuple[int, int, int]:
|
|
102
|
+
"""Non-blank in-scope lines, split into (primary, test, docs).
|
|
102
103
|
|
|
103
104
|
The split exists because the score's denominator has to move with its
|
|
104
105
|
numerator. Scoring primary-tree findings over a LOC count that included the
|
|
@@ -106,6 +107,12 @@ def loc_under(
|
|
|
106
107
|
tested — the same numerator/denominator mismatch that `exclude_patterns`
|
|
107
108
|
already caused once, arriving by a different door.
|
|
108
109
|
|
|
110
|
+
Documentation is split for the same reason, and was not: its *findings*
|
|
111
|
+
move to their own axis and out of the score, while its *lines* stayed in
|
|
112
|
+
the primary denominator. FastAPI carries 7,160 lines of `docs/en/data/`
|
|
113
|
+
— translator and contributor lists — diluting the count its code is
|
|
114
|
+
graded against. Third occurrence of one mismatch.
|
|
115
|
+
|
|
109
116
|
`skip` names the run's own artifacts — the report, the baseline, the
|
|
110
117
|
suppressions file. Dropping their *findings* without dropping their
|
|
111
118
|
*lines* is that same mismatch a third time: an audit that wrote a
|
|
@@ -114,9 +121,11 @@ def loc_under(
|
|
|
114
121
|
unchanged repository returned 0.00 and 4.25.
|
|
115
122
|
"""
|
|
116
123
|
test_patterns = tuple(test_patterns)
|
|
124
|
+
docs_patterns = tuple(docs_patterns)
|
|
117
125
|
skip = {p.resolve() for p in skip}
|
|
118
126
|
primary = 0
|
|
119
127
|
test = 0
|
|
128
|
+
docs = 0
|
|
120
129
|
# `rglob` on a file yields nothing, so a single-file audit reported zero
|
|
121
130
|
# lines — and a zero denominator is not normalised at all, so the grade
|
|
122
131
|
# became the raw subtotal. Auditing one file is supported; it should be
|
|
@@ -138,6 +147,8 @@ def loc_under(
|
|
|
138
147
|
lines = sum(1 for line in text.splitlines() if line.strip())
|
|
139
148
|
if test_patterns and is_test_path(path, root, test_patterns):
|
|
140
149
|
test += lines
|
|
150
|
+
elif docs_patterns and is_test_path(path, root, docs_patterns):
|
|
151
|
+
docs += lines
|
|
141
152
|
else:
|
|
142
153
|
primary += lines
|
|
143
|
-
return primary, test
|
|
154
|
+
return primary, test, docs
|
|
@@ -691,7 +691,26 @@ def _gate_fail_on_new(
|
|
|
691
691
|
]
|
|
692
692
|
if new_findings:
|
|
693
693
|
tripped.append("fail_on_new")
|
|
694
|
-
|
|
694
|
+
# What "new" means depends on whether there is anything to be new
|
|
695
|
+
# *against*. Saying "since baseline" when no baseline exists claims
|
|
696
|
+
# these findings appeared after one, which is the opposite of the
|
|
697
|
+
# truth on a first run.
|
|
698
|
+
baseline_state = gate_config.get("_baseline_state")
|
|
699
|
+
if baseline_state == "absent":
|
|
700
|
+
reasons.append(
|
|
701
|
+
f"{len(new_findings)} finding(s), and no baseline exists yet — on a first "
|
|
702
|
+
f"run everything is new because there is nothing to compare against. "
|
|
703
|
+
f"Work the order, then re-run with --bump-baseline to accept what is "
|
|
704
|
+
f"left and gate on regressions from there."
|
|
705
|
+
)
|
|
706
|
+
elif baseline_state == "unreadable":
|
|
707
|
+
reasons.append(
|
|
708
|
+
f"{len(new_findings)} finding(s) read as new because the baseline file "
|
|
709
|
+
f"could not be parsed. Fix or delete it — a broken baseline silently "
|
|
710
|
+
f"turns an established repository back into a first run."
|
|
711
|
+
)
|
|
712
|
+
else:
|
|
713
|
+
reasons.append(f"{len(new_findings)} new finding(s) since baseline")
|
|
695
714
|
|
|
696
715
|
|
|
697
716
|
def _gate_min_score(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/requires.txt
RENAMED
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_agent.egg-info/top_level.txt
RENAMED
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/data/semgrep-offline.yaml
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
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/__init__.py
RENAMED
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/bandit_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/builtin_rules.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/gosec_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/osv_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{secure_code_agent-0.8.0 → secure_code_agent-0.9.0}/src/secure_code_audit/scanners/trivy_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|