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,83 @@
1
+ """pip-audit — Python SCA via PyPI Advisory DB / OSV.
2
+
3
+ Invocation:
4
+ pip-audit -r requirements.txt --format=json
5
+
6
+ Output: JSON with dependencies[] each having vulns[] with id, fix_versions,
7
+ description.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ from secure_code_audit.config import Config
15
+ from secure_code_audit.findings import Category, Confidence, Finding, Severity
16
+ from secure_code_audit.scanners.base import Scanner
17
+
18
+
19
+ _REQ_NAMES: tuple[str, ...] = (
20
+ "requirements.txt",
21
+ "requirements-dev.txt",
22
+ "requirements/base.txt",
23
+ )
24
+
25
+
26
+ class PipAuditScanner(Scanner):
27
+ name = "pip_audit"
28
+ binary = "pip-audit"
29
+ default_category = Category.DEPENDENCIES
30
+
31
+ def run(self, target: Path, config: Config) -> list[Finding]:
32
+ if not self.is_available():
33
+ return [self._unavailable_finding(target)]
34
+
35
+ req_files = [target / r for r in _REQ_NAMES if (target / r).exists()]
36
+ if not req_files:
37
+ return [self._make_finding(
38
+ rule_id=f"{self.name}.no_requirements",
39
+ message="No requirements.txt found at target root; pip-audit skipped.",
40
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
41
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
42
+ category=Category.DEPENDENCIES,
43
+ )]
44
+
45
+ sc_cfg = self.cfg(config)
46
+ findings: list[Finding] = []
47
+ for req in req_files:
48
+ args = [self.binary, "-r", str(req), "--format=json"]
49
+ args.extend(sc_cfg.extra_args)
50
+ r = self._exec(args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds,
51
+ allowed_exits=(0, 1))
52
+ if r.returncode == 124:
53
+ findings.append(self._make_finding(
54
+ rule_id=f"{self.name}.tool_timeout",
55
+ message=f"pip-audit timed out on {req.name}: {r.stderr}",
56
+ file_path=req, line_start=0, line_end=None, code_snippet=None,
57
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
58
+ ))
59
+ continue
60
+ if not r.stdout.strip():
61
+ continue
62
+ try:
63
+ payload = json.loads(r.stdout)
64
+ except json.JSONDecodeError:
65
+ continue
66
+
67
+ deps = payload.get("dependencies", payload if isinstance(payload, list) else [])
68
+ for dep in deps:
69
+ name = dep.get("name", "?")
70
+ version = dep.get("version", "?")
71
+ for vuln in dep.get("vulns", []) or []:
72
+ vuln_id = vuln.get("id", "UNKNOWN")
73
+ fixes = vuln.get("fix_versions") or []
74
+ desc = (vuln.get("description") or "").strip()
75
+ fix_note = f" Fix in: {', '.join(fixes)}" if fixes else " No fix available."
76
+ findings.append(self._make_finding(
77
+ rule_id=f"pip_audit.{vuln_id}",
78
+ message=f"{name} {version} — {vuln_id}: {desc[:200]}{fix_note}",
79
+ file_path=req, line_start=0, line_end=None, code_snippet=None,
80
+ severity=Severity.HIGH, confidence=Confidence.HIGH,
81
+ category=Category.DEPENDENCIES,
82
+ ))
83
+ return findings
@@ -0,0 +1,156 @@
1
+ """OpenSSF Scorecard — repo + supply-chain hygiene.
2
+
3
+ Special case among scanners:
4
+ · Operates on a REMOTE repo URL, not a local path.
5
+ · Needs the GH_TOKEN env var (or equivalent) to query GitHub APIs.
6
+ · Best run on the repo root, not on a subdirectory.
7
+
8
+ Invocation:
9
+ scorecard --repo=<github-url> --format=json --show-details
10
+
11
+ Output: JSON with `checks[]` array, each having `name`, `score` (0-10
12
+ or -1 for inconclusive), `reason`, `details[]`, and `documentation.url`.
13
+
14
+ We map each Scorecard check into `supply_chain` (or `policy_docs` for
15
+ documentation-style checks). Score → severity mapping:
16
+ · score < 0 → INFORMATIONAL (inconclusive — check couldn't run)
17
+ · score < 3 → HIGH (clearly failing)
18
+ · score < 7 → MEDIUM (warning)
19
+ · score < 10 → LOW (acceptable but not perfect)
20
+ · score == 10 → no finding emitted (passed cleanly)
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import subprocess
26
+ from pathlib import Path
27
+
28
+ from secure_code_audit.config import Config
29
+ from secure_code_audit.findings import Category, Confidence, Finding, Severity
30
+ from secure_code_audit.scanners.base import Scanner
31
+
32
+
33
+ # Checks whose semantics are "documentation present" rather than
34
+ # supply-chain integrity.
35
+ _POLICY_DOCS_CHECKS = {"Security-Policy", "License", "CII-Best-Practices"}
36
+
37
+
38
+ class ScorecardScanner(Scanner):
39
+ name = "scorecard"
40
+ binary = "scorecard"
41
+ default_category = Category.SUPPLY_CHAIN
42
+
43
+ def run(self, target: Path, config: Config) -> list[Finding]:
44
+ if not self.is_available():
45
+ return [self._unavailable_finding(target)]
46
+
47
+ repo_url = self._infer_repo_url(target)
48
+ if repo_url is None:
49
+ return [self._make_finding(
50
+ rule_id=f"{self.name}.no_remote",
51
+ message="Scorecard requires a remote GitHub URL (origin remote not found).",
52
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
53
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
54
+ category=Category.SUPPLY_CHAIN,
55
+ )]
56
+
57
+ sc_cfg = self.cfg(config)
58
+ args = [
59
+ self.binary,
60
+ f"--repo={repo_url}",
61
+ "--format=json", "--show-details",
62
+ ]
63
+ args.extend(sc_cfg.extra_args)
64
+
65
+ r = self._exec(args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds,
66
+ allowed_exits=(0, 1, 2))
67
+ if r.returncode == 124:
68
+ return [self._make_finding(
69
+ rule_id=f"{self.name}.tool_timeout",
70
+ message=f"scorecard timed out: {r.stderr[:200]}",
71
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
72
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
73
+ )]
74
+ if not r.stdout.strip():
75
+ return []
76
+
77
+ try:
78
+ payload = json.loads(r.stdout)
79
+ except json.JSONDecodeError:
80
+ return []
81
+
82
+ return self._parse(payload, target)
83
+
84
+ def _infer_repo_url(self, target: Path) -> str | None:
85
+ """Run `git remote get-url origin` in target. Returns the URL
86
+ normalized to https://github.com/<owner>/<repo> form, or None
87
+ if no GitHub origin is present."""
88
+ try:
89
+ r = subprocess.run(
90
+ ["git", "remote", "get-url", "origin"],
91
+ cwd=str(target),
92
+ check=False, capture_output=True, text=True, timeout=5,
93
+ )
94
+ except (FileNotFoundError, subprocess.TimeoutExpired):
95
+ return None
96
+ url = (r.stdout or "").strip()
97
+ if not url:
98
+ return None
99
+ # Normalize SSH form to HTTPS form so scorecard accepts it.
100
+ if url.startswith("git@github.com:"):
101
+ url = "https://github.com/" + url[len("git@github.com:"):]
102
+ if url.endswith(".git"):
103
+ url = url[:-4]
104
+ if not url.startswith("https://github.com/"):
105
+ return None
106
+ return url
107
+
108
+ def _parse(self, payload: dict, target: Path) -> list[Finding]:
109
+ out: list[Finding] = []
110
+ for check in payload.get("checks", []):
111
+ name = str(check.get("name") or "Unknown")
112
+ score = check.get("score")
113
+ reason = (check.get("reason") or "").strip()
114
+ doc = (check.get("documentation") or {}).get("url") or ""
115
+
116
+ severity = self._score_to_severity(score)
117
+ if severity is None:
118
+ continue # passed cleanly, no finding
119
+
120
+ category = (Category.POLICY_DOCS
121
+ if name in _POLICY_DOCS_CHECKS
122
+ else Category.SUPPLY_CHAIN)
123
+ msg = f"OpenSSF Scorecard `{name}` scored {score}/10: {reason}"
124
+ if doc:
125
+ msg = f"{msg} [{doc}]"
126
+
127
+ out.append(self._make_finding(
128
+ rule_id=f"scorecard.{name}",
129
+ message=msg,
130
+ file_path=target,
131
+ line_start=0,
132
+ line_end=None,
133
+ code_snippet=None,
134
+ severity=severity,
135
+ confidence=Confidence.HIGH,
136
+ category=category,
137
+ ))
138
+ return out
139
+
140
+ @staticmethod
141
+ def _score_to_severity(score) -> Severity | None:
142
+ if score is None:
143
+ return None
144
+ try:
145
+ s = int(score)
146
+ except (TypeError, ValueError):
147
+ return None
148
+ if s < 0:
149
+ return Severity.INFORMATIONAL
150
+ if s == 10:
151
+ return None
152
+ if s < 3:
153
+ return Severity.HIGH
154
+ if s < 7:
155
+ return Severity.MEDIUM
156
+ return Severity.LOW
@@ -0,0 +1,119 @@
1
+ """Semgrep — multi-language SAST.
2
+
3
+ Invocation:
4
+ semgrep --config=auto --sarif --metrics=off --error <target>
5
+
6
+ Output: SARIF 2.1.0. We piggyback the canonical SARIF parser since other
7
+ scanners (CodeQL, Snyk, Trivy) emit the same format.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import tempfile
13
+ from pathlib import Path
14
+
15
+ from secure_code_audit.config import Config
16
+ from secure_code_audit.findings import Category, Confidence, Finding, Severity
17
+ from secure_code_audit.scanners.base import Scanner
18
+
19
+
20
+ class SemgrepScanner(Scanner):
21
+ name = "semgrep"
22
+ binary = "semgrep"
23
+ default_category = Category.CODE_VULNERABILITIES
24
+
25
+ def run(self, target: Path, config: Config) -> list[Finding]:
26
+ if not self.is_available():
27
+ return [self._unavailable_finding(target)]
28
+
29
+ sc_cfg = self.cfg(config)
30
+ config_arg = "auto" # registry-curated pack
31
+ if not sc_cfg.online:
32
+ # Operator opted out of online registry — fall back to bundled
33
+ # rules. Semgrep ships a small offline set under p/python +
34
+ # p/javascript when --config points to those names.
35
+ config_arg = "p/security-audit"
36
+
37
+ with tempfile.NamedTemporaryFile(suffix=".sarif", delete=False) as tmp:
38
+ sarif_path = Path(tmp.name)
39
+ try:
40
+ args = [
41
+ self.binary,
42
+ "--config", config_arg,
43
+ "--sarif", "--metrics=off",
44
+ "--output", str(sarif_path),
45
+ str(target),
46
+ ]
47
+ args.extend(sc_cfg.extra_args)
48
+ r = self._exec(args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds,
49
+ allowed_exits=(0, 1))
50
+ if r.returncode == 124:
51
+ return [self._make_finding(
52
+ rule_id=f"{self.name}.tool_timeout",
53
+ message=f"semgrep timed out: {r.stderr[:200]}",
54
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
55
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
56
+ )]
57
+ if not sarif_path.exists():
58
+ return []
59
+ try:
60
+ payload = json.loads(sarif_path.read_text(encoding="utf-8"))
61
+ except json.JSONDecodeError:
62
+ return []
63
+ return self._parse_sarif(payload, target)
64
+ finally:
65
+ sarif_path.unlink(missing_ok=True)
66
+
67
+ # -- minimal SARIF parser; a fuller one lives in sarif.py for ingest --
68
+ def _parse_sarif(self, payload: dict, target: Path) -> list[Finding]:
69
+ findings: list[Finding] = []
70
+ for run in payload.get("runs", []):
71
+ rules = {
72
+ r.get("id"): r
73
+ for r in (run.get("tool", {}).get("driver", {}).get("rules", []) or [])
74
+ }
75
+ for result in run.get("results", []):
76
+ rule_id = result.get("ruleId") or "unknown"
77
+ rule = rules.get(rule_id, {})
78
+ level = result.get("level") or rule.get("defaultConfiguration", {}).get("level") or "warning"
79
+ msg = (result.get("message") or {}).get("text") or rule.get("shortDescription", {}).get("text") or rule_id
80
+
81
+ locs = result.get("locations") or []
82
+ if not locs:
83
+ continue
84
+ loc = locs[0]
85
+ phys = loc.get("physicalLocation") or {}
86
+ file_uri = (phys.get("artifactLocation") or {}).get("uri") or ""
87
+ region = phys.get("region") or {}
88
+ line_start = int(region.get("startLine") or 0)
89
+ line_end = int(region.get("endLine") or line_start)
90
+ snippet = (region.get("snippet") or {}).get("text")
91
+
92
+ # SARIF severity: "error" → HIGH, "warning" → MEDIUM, "note" → LOW.
93
+ if level == "error":
94
+ severity = Severity.HIGH
95
+ elif level == "note":
96
+ severity = Severity.LOW
97
+ else:
98
+ severity = Severity.MEDIUM
99
+
100
+ # Pull CWE from rule properties.cwe if present.
101
+ cwe = None
102
+ props = rule.get("properties") or {}
103
+ if isinstance(props.get("cwe"), list) and props["cwe"]:
104
+ cwe = props["cwe"][0]
105
+ elif isinstance(props.get("cwe"), str):
106
+ cwe = props["cwe"]
107
+
108
+ findings.append(self._make_finding(
109
+ rule_id=rule_id,
110
+ message=msg,
111
+ file_path=Path(file_uri) if file_uri else target,
112
+ line_start=line_start,
113
+ line_end=line_end if line_end != line_start else None,
114
+ code_snippet=snippet,
115
+ severity=severity,
116
+ confidence=Confidence.MEDIUM,
117
+ cwe_override=cwe,
118
+ ))
119
+ return findings
@@ -0,0 +1,87 @@
1
+ """Trivy — containers / filesystem / IaC / Kubernetes scanner.
2
+
3
+ Invocation:
4
+ trivy fs --format sarif --output <tmp> --quiet --no-progress <target>
5
+
6
+ Trivy's `fs` subcommand covers filesystem scanning for vulnerable deps
7
+ (via its own DB ingest of OSV/GHSA/NVD), secrets, misconfigurations
8
+ (Terraform / CloudFormation / Dockerfile / Helm / k8s), and license
9
+ issues. We focus on the security-relevant categories: vulnerabilities,
10
+ misconfigs, secrets.
11
+
12
+ SARIF output goes through `secure_code_audit.sarif.ingest()` so we get
13
+ canonical Findings with the same normalization as every other SARIF
14
+ source.
15
+
16
+ Trivy emits a single SARIF file per scan with results across all
17
+ categories — the standards mapping table routes findings into the
18
+ right `Category` based on rule properties.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import tempfile
23
+ from dataclasses import replace
24
+ from pathlib import Path
25
+
26
+ from secure_code_audit.config import Config
27
+ from secure_code_audit.findings import Category, Confidence, Finding, Severity
28
+ from secure_code_audit.sarif import ingest as sarif_ingest
29
+ from secure_code_audit.scanners.base import Scanner
30
+
31
+
32
+ class TrivyScanner(Scanner):
33
+ name = "trivy"
34
+ binary = "trivy"
35
+ default_category = Category.CONFIG_IAC
36
+
37
+ def run(self, target: Path, config: Config) -> list[Finding]:
38
+ if not self.is_available():
39
+ return [self._unavailable_finding(target)]
40
+
41
+ sc_cfg = self.cfg(config)
42
+ with tempfile.NamedTemporaryFile(suffix=".sarif", delete=False) as tmp:
43
+ sarif_path = Path(tmp.name)
44
+ try:
45
+ args = [
46
+ self.binary, "fs",
47
+ "--format", "sarif",
48
+ "--output", str(sarif_path),
49
+ "--quiet", "--no-progress",
50
+ # Skip license findings — we focus on security only.
51
+ "--scanners", "vuln,secret,misconfig",
52
+ str(target),
53
+ ]
54
+ args.extend(sc_cfg.extra_args)
55
+
56
+ r = self._exec(args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds,
57
+ allowed_exits=(0, 1))
58
+ if r.returncode == 124:
59
+ return [self._make_finding(
60
+ rule_id=f"{self.name}.tool_timeout",
61
+ message=f"trivy timed out: {r.stderr[:200]}",
62
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
63
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
64
+ )]
65
+ if not sarif_path.exists() or sarif_path.stat().st_size == 0:
66
+ return []
67
+
68
+ ingested = sarif_ingest(sarif_path, default_scanner="trivy")
69
+ # Trivy tags its rules with a category prefix (CVE-, AVD-, etc.);
70
+ # route them into the right Category bucket.
71
+ return [self._route_category(f) for f in ingested]
72
+ finally:
73
+ sarif_path.unlink(missing_ok=True)
74
+
75
+ def _route_category(self, f: Finding) -> Finding:
76
+ """Trivy bundles three rule families into one SARIF output.
77
+ Route by rule_id prefix so the scoring layer puts them in the
78
+ right category."""
79
+ rid = f.rule_id.upper()
80
+ if rid.startswith("CVE-") or rid.startswith("GHSA-"):
81
+ return replace(f, category=Category.DEPENDENCIES, scanner="trivy")
82
+ if rid.startswith("AVD-") or "MISCONFIG" in rid:
83
+ return replace(f, category=Category.CONFIG_IAC, scanner="trivy")
84
+ if "SECRET" in rid or rid.startswith("AWS") or rid.startswith("PRIVATE-KEY"):
85
+ return replace(f, category=Category.SECRETS, scanner="trivy",
86
+ severity=Severity.CRITICAL)
87
+ return replace(f, scanner="trivy")
@@ -0,0 +1,91 @@
1
+ """TruffleHog — secret scanning with verifiers.
2
+
3
+ Invocation:
4
+ trufflehog filesystem --json --no-update --only-verified <target>
5
+
6
+ `--only-verified` is the default — high-precision matches where
7
+ TruffleHog's verifier confirmed the secret is currently live against
8
+ the upstream service. Operators can opt into unverified findings via
9
+ `scanners.trufflehog.extra_args: ["--no-only-verified"]` in config.
10
+
11
+ Output: JSONL (one JSON object per line) — distinct from gitleaks
12
+ which writes a single JSON array.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from pathlib import Path
18
+
19
+ from secure_code_audit.config import Config
20
+ from secure_code_audit.findings import Category, Confidence, Finding, Severity
21
+ from secure_code_audit.scanners.base import Scanner
22
+
23
+
24
+ class TruffleHogScanner(Scanner):
25
+ name = "trufflehog"
26
+ binary = "trufflehog"
27
+ default_category = Category.SECRETS
28
+
29
+ def run(self, target: Path, config: Config) -> list[Finding]:
30
+ if not self.is_available():
31
+ return [self._unavailable_finding(target)]
32
+
33
+ sc_cfg = self.cfg(config)
34
+ args = [
35
+ self.binary, "filesystem",
36
+ "--json", "--no-update",
37
+ "--only-verified",
38
+ str(target),
39
+ ]
40
+ args.extend(sc_cfg.extra_args)
41
+
42
+ r = self._exec(args, cwd=target, timeout_seconds=sc_cfg.timeout_seconds,
43
+ allowed_exits=(0, 183)) # 183 = findings present
44
+ if r.returncode == 124:
45
+ return [self._make_finding(
46
+ rule_id=f"{self.name}.tool_timeout",
47
+ message=f"trufflehog timed out: {r.stderr[:200]}",
48
+ file_path=target, line_start=0, line_end=None, code_snippet=None,
49
+ severity=Severity.INFORMATIONAL, confidence=Confidence.HIGH,
50
+ )]
51
+ if not r.stdout.strip():
52
+ return []
53
+
54
+ out: list[Finding] = []
55
+ for line in r.stdout.splitlines():
56
+ line = line.strip()
57
+ if not line:
58
+ continue
59
+ try:
60
+ hit = json.loads(line)
61
+ except json.JSONDecodeError:
62
+ continue
63
+ out.append(self._parse_one(hit))
64
+ return out
65
+
66
+ def _parse_one(self, hit: dict) -> Finding:
67
+ detector = str(hit.get("DetectorName") or hit.get("Detector") or "unknown")
68
+ # SourceMetadata.Data.Filesystem.file / line
69
+ meta = (((hit.get("SourceMetadata") or {}).get("Data") or {}).get("Filesystem") or {})
70
+ file_path = Path(meta.get("file") or "")
71
+ line = int(meta.get("line") or 0)
72
+ verified = bool(hit.get("Verified"))
73
+ # The redacted match is what TruffleHog emits when --only-verified
74
+ # is on — it strips the raw secret bytes.
75
+ match = str(hit.get("Redacted") or "<verified>").strip()
76
+
77
+ # Verified secrets are CRITICAL; unverified (if operator opted in
78
+ # via --no-only-verified in extra_args) are HIGH.
79
+ severity = Severity.CRITICAL if verified else Severity.HIGH
80
+
81
+ return self._make_finding(
82
+ rule_id=f"trufflehog.{detector}",
83
+ message=f"{detector}: {match}".strip(),
84
+ file_path=file_path,
85
+ line_start=line,
86
+ line_end=None,
87
+ code_snippet=match[:200],
88
+ severity=severity,
89
+ confidence=Confidence.HIGH,
90
+ category=Category.SECRETS,
91
+ )