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.
Files changed (33) hide show
  1. secure_code_agent-0.2.0.dist-info/METADATA +328 -0
  2. secure_code_agent-0.2.0.dist-info/RECORD +33 -0
  3. secure_code_agent-0.2.0.dist-info/WHEEL +5 -0
  4. secure_code_agent-0.2.0.dist-info/entry_points.txt +3 -0
  5. secure_code_agent-0.2.0.dist-info/licenses/LICENSE +21 -0
  6. secure_code_agent-0.2.0.dist-info/top_level.txt +1 -0
  7. secure_code_audit/__init__.py +3 -0
  8. secure_code_audit/baseline.py +110 -0
  9. secure_code_audit/cli.py +258 -0
  10. secure_code_audit/config.py +115 -0
  11. secure_code_audit/findings.py +165 -0
  12. secure_code_audit/git_tools.py +82 -0
  13. secure_code_audit/instructions.py +141 -0
  14. secure_code_audit/remediation.py +168 -0
  15. secure_code_audit/renderers.py +253 -0
  16. secure_code_audit/sarif.py +221 -0
  17. secure_code_audit/scanners/__init__.py +50 -0
  18. secure_code_audit/scanners/bandit_scanner.py +83 -0
  19. secure_code_audit/scanners/base.py +194 -0
  20. secure_code_audit/scanners/builtin_rules.py +183 -0
  21. secure_code_audit/scanners/checkov_scanner.py +69 -0
  22. secure_code_audit/scanners/gitleaks_scanner.py +86 -0
  23. secure_code_audit/scanners/hadolint_scanner.py +107 -0
  24. secure_code_audit/scanners/npm_audit_scanner.py +101 -0
  25. secure_code_audit/scanners/osv_scanner.py +108 -0
  26. secure_code_audit/scanners/pip_audit_scanner.py +83 -0
  27. secure_code_audit/scanners/scorecard_scanner.py +156 -0
  28. secure_code_audit/scanners/semgrep_scanner.py +119 -0
  29. secure_code_audit/scanners/trivy_scanner.py +87 -0
  30. secure_code_audit/scanners/trufflehog_scanner.py +91 -0
  31. secure_code_audit/scoring.py +280 -0
  32. secure_code_audit/standards.py +391 -0
  33. 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