secure-code-agent 0.2.0__py3-none-any.whl
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.2.0.dist-info/METADATA +328 -0
- secure_code_agent-0.2.0.dist-info/RECORD +33 -0
- secure_code_agent-0.2.0.dist-info/WHEEL +5 -0
- secure_code_agent-0.2.0.dist-info/entry_points.txt +3 -0
- secure_code_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
- secure_code_agent-0.2.0.dist-info/top_level.txt +1 -0
- secure_code_audit/__init__.py +3 -0
- secure_code_audit/baseline.py +110 -0
- secure_code_audit/cli.py +258 -0
- secure_code_audit/config.py +115 -0
- secure_code_audit/findings.py +165 -0
- secure_code_audit/git_tools.py +82 -0
- secure_code_audit/instructions.py +141 -0
- secure_code_audit/remediation.py +168 -0
- secure_code_audit/renderers.py +253 -0
- secure_code_audit/sarif.py +221 -0
- secure_code_audit/scanners/__init__.py +50 -0
- secure_code_audit/scanners/bandit_scanner.py +83 -0
- secure_code_audit/scanners/base.py +194 -0
- secure_code_audit/scanners/builtin_rules.py +183 -0
- secure_code_audit/scanners/checkov_scanner.py +69 -0
- secure_code_audit/scanners/gitleaks_scanner.py +86 -0
- secure_code_audit/scanners/hadolint_scanner.py +107 -0
- secure_code_audit/scanners/npm_audit_scanner.py +101 -0
- secure_code_audit/scanners/osv_scanner.py +108 -0
- secure_code_audit/scanners/pip_audit_scanner.py +83 -0
- secure_code_audit/scanners/scorecard_scanner.py +156 -0
- secure_code_audit/scanners/semgrep_scanner.py +119 -0
- secure_code_audit/scanners/trivy_scanner.py +87 -0
- secure_code_audit/scanners/trufflehog_scanner.py +91 -0
- secure_code_audit/scoring.py +280 -0
- secure_code_audit/standards.py +391 -0
- secure_code_audit/suppressions.py +175 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Suppressions loader — `.scignore.yaml`.
|
|
2
|
+
|
|
3
|
+
Schema:
|
|
4
|
+
- file: <single file path> (optional)
|
|
5
|
+
paths: [<glob>, <glob>] (optional — alternative to `file`)
|
|
6
|
+
rule_id: <rule id> (required; "*" allowed only with file/paths)
|
|
7
|
+
reason: <non-empty string> (required)
|
|
8
|
+
expires: <ISO date YYYY-MM-DD> (required; max 365 days from today)
|
|
9
|
+
|
|
10
|
+
Wildcard rule (rule_id: "*") requires `file` or `paths` so an operator can't
|
|
11
|
+
disable a rule globally. Past-expiry entries become CRITICAL findings.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime
|
|
16
|
+
import fnmatch
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from secure_code_audit.findings import Category, Confidence, Finding, Severity
|
|
22
|
+
|
|
23
|
+
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
24
|
+
_MAX_TTL_DAYS = 365
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class SuppressionRule:
|
|
29
|
+
rule_id: str
|
|
30
|
+
reason: str
|
|
31
|
+
expires: datetime.date
|
|
32
|
+
file: str | None = None
|
|
33
|
+
paths: tuple[str, ...] = field(default_factory=tuple)
|
|
34
|
+
|
|
35
|
+
def matches(self, finding: Finding) -> bool:
|
|
36
|
+
if self.rule_id != "*" and self.rule_id != finding.rule_id:
|
|
37
|
+
return False
|
|
38
|
+
rel = finding.file_path.as_posix()
|
|
39
|
+
if self.file is not None and self.file != rel and not rel.endswith(self.file):
|
|
40
|
+
return False
|
|
41
|
+
if self.paths:
|
|
42
|
+
if not any(fnmatch.fnmatch(rel, p) for p in self.paths):
|
|
43
|
+
return False
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def expired(self) -> bool:
|
|
48
|
+
return datetime.date.today() > self.expires
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load(path: Path) -> tuple[list[SuppressionRule], list[str]]:
|
|
52
|
+
"""Returns (rules, validation_errors). On a missing file, both are empty."""
|
|
53
|
+
if not path.exists():
|
|
54
|
+
return [], []
|
|
55
|
+
try:
|
|
56
|
+
import yaml # pyyaml — only imported when a suppressions file is present
|
|
57
|
+
except ImportError:
|
|
58
|
+
return [], [f"{path}: pyyaml not installed — `pip install pyyaml` to use .scignore.yaml"]
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or []
|
|
62
|
+
except yaml.YAMLError as e:
|
|
63
|
+
return [], [f"{path}: YAML parse error: {e}"]
|
|
64
|
+
|
|
65
|
+
if not isinstance(raw, list):
|
|
66
|
+
return [], [f"{path}: top-level must be a list of suppression entries."]
|
|
67
|
+
|
|
68
|
+
rules: list[SuppressionRule] = []
|
|
69
|
+
errors: list[str] = []
|
|
70
|
+
today = datetime.date.today()
|
|
71
|
+
|
|
72
|
+
for i, entry in enumerate(raw):
|
|
73
|
+
if not isinstance(entry, dict):
|
|
74
|
+
errors.append(f"{path}: entry #{i}: must be a mapping.")
|
|
75
|
+
continue
|
|
76
|
+
rule_id = str(entry.get("rule_id", "")).strip()
|
|
77
|
+
if not rule_id:
|
|
78
|
+
errors.append(f"{path}: entry #{i}: 'rule_id' is required.")
|
|
79
|
+
continue
|
|
80
|
+
reason = str(entry.get("reason", "")).strip()
|
|
81
|
+
if not reason:
|
|
82
|
+
errors.append(f"{path}: entry #{i}: 'reason' is required (non-empty).")
|
|
83
|
+
continue
|
|
84
|
+
expires_raw = str(entry.get("expires", "")).strip()
|
|
85
|
+
if not _DATE_RE.match(expires_raw):
|
|
86
|
+
errors.append(f"{path}: entry #{i}: 'expires' is required as YYYY-MM-DD.")
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
expires = datetime.date.fromisoformat(expires_raw)
|
|
90
|
+
except ValueError:
|
|
91
|
+
errors.append(f"{path}: entry #{i}: 'expires' not parseable.")
|
|
92
|
+
continue
|
|
93
|
+
if (expires - today).days > _MAX_TTL_DAYS:
|
|
94
|
+
errors.append(
|
|
95
|
+
f"{path}: entry #{i}: 'expires' must be within {_MAX_TTL_DAYS} days "
|
|
96
|
+
f"(got {(expires - today).days} days out)."
|
|
97
|
+
)
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
file_v = entry.get("file")
|
|
101
|
+
paths_v = entry.get("paths") or []
|
|
102
|
+
if rule_id == "*" and not file_v and not paths_v:
|
|
103
|
+
errors.append(
|
|
104
|
+
f"{path}: entry #{i}: rule_id='*' requires `file` or `paths`."
|
|
105
|
+
)
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
rules.append(SuppressionRule(
|
|
109
|
+
rule_id=rule_id,
|
|
110
|
+
reason=reason,
|
|
111
|
+
expires=expires,
|
|
112
|
+
file=str(file_v) if file_v else None,
|
|
113
|
+
paths=tuple(str(p) for p in paths_v) if paths_v else (),
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
return rules, errors
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def apply(findings: list[Finding], rules: list[SuppressionRule]) -> list[Finding]:
|
|
120
|
+
"""Mark matching findings as suppressed (set suppressed=True + note).
|
|
121
|
+
Expired rules do NOT suppress — but generate their own findings via
|
|
122
|
+
`expired_findings()`. Returns a new list (frozen dataclass replace)."""
|
|
123
|
+
from dataclasses import replace
|
|
124
|
+
out: list[Finding] = []
|
|
125
|
+
for f in findings:
|
|
126
|
+
active = next((r for r in rules if not r.expired and r.matches(f)), None)
|
|
127
|
+
if active is not None:
|
|
128
|
+
out.append(replace(
|
|
129
|
+
f,
|
|
130
|
+
suppressed=True,
|
|
131
|
+
suppression_note=f"{active.reason} (expires {active.expires.isoformat()})",
|
|
132
|
+
))
|
|
133
|
+
else:
|
|
134
|
+
out.append(f)
|
|
135
|
+
return out
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def expired_findings(rules: list[SuppressionRule], path: Path) -> list[Finding]:
|
|
139
|
+
"""One CRITICAL finding per expired suppression — you can't ship
|
|
140
|
+
`reason: 'we'll fix it later'` forever."""
|
|
141
|
+
out: list[Finding] = []
|
|
142
|
+
for r in rules:
|
|
143
|
+
if not r.expired:
|
|
144
|
+
continue
|
|
145
|
+
rid = f"suppressions.expired.{r.rule_id}"
|
|
146
|
+
snippet = f"rule_id: {r.rule_id}; expired {r.expires.isoformat()}; reason: {r.reason}"
|
|
147
|
+
out.append(Finding(
|
|
148
|
+
rule_id=rid,
|
|
149
|
+
scanner="suppressions",
|
|
150
|
+
fingerprint=Finding.make_fingerprint(
|
|
151
|
+
canonical_cwe=None,
|
|
152
|
+
rule_id=rid,
|
|
153
|
+
file_path=path,
|
|
154
|
+
code_snippet=snippet,
|
|
155
|
+
),
|
|
156
|
+
canonical_cwe=None,
|
|
157
|
+
owasp_top10=None,
|
|
158
|
+
asvs_section=None,
|
|
159
|
+
nist_ssdf="PO.4.1",
|
|
160
|
+
category=Category.POLICY_DOCS,
|
|
161
|
+
severity=Severity.CRITICAL,
|
|
162
|
+
confidence=Confidence.HIGH,
|
|
163
|
+
file_path=path,
|
|
164
|
+
line_start=0,
|
|
165
|
+
line_end=None,
|
|
166
|
+
code_snippet=snippet,
|
|
167
|
+
message=(
|
|
168
|
+
f"Suppression for rule `{r.rule_id}` expired on "
|
|
169
|
+
f"{r.expires.isoformat()}: \"{r.reason}\". Either fix the "
|
|
170
|
+
"underlying issue or extend `expires` with a fresh reason."
|
|
171
|
+
),
|
|
172
|
+
short_desc="Expired suppression entry.",
|
|
173
|
+
fix_hint="Address the original finding, or extend the suppression with operator approval.",
|
|
174
|
+
))
|
|
175
|
+
return out
|