Fix: Security-Scan-Report robuster gemacht & TruffleHog läuft bei jedem Push
Security Scans / Trivy & OSV-Scanner (push) Failing after 5m27s
TruffleHog Secret Scan / TruffleHog (push) Successful in 7s

- OSV-Scanner-Funde erhalten jetzt eine echte Severity (aus CVSS-Score
  der Gruppen abgeleitet) statt hartcodiert "-", damit kritische Funde
  nicht unten in der Tabelle verschwinden
- Unerwartete Exit-Codes von osv-scanner/trufflehog gelten jetzt als
  Fehlschlag statt stillschweigend als "keine Funde"
- Timeout für Gitea-API-Requests ergänzt
- Label-Suche paginiert jetzt vollständig statt nur die ersten 50
- Markdown-Tabellenzellen werden escaped (Pipe/Zeilenumbruch)
- Doppeltes Sortieren der Findings entfernt, Dict-Aufbau der Loader
  über gemeinsamen Helper vereinheitlicht
- trufflehog-scan.yaml läuft nun bei jedem Push, nicht nur auf
  main/testing/dev

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKMjR9pBxshZYZdhKyT5Ky
This commit is contained in:
2026-09-09 23:00:41 +02:00
co-authored by Claude Sonnet 5
parent 9445a82b53
commit 134d3048cf
2 changed files with 107 additions and 63 deletions
-4
View File
@@ -2,10 +2,6 @@ name: TruffleHog Secret Scan
on: on:
push: push:
branches:
- main
- testing
- dev
pull_request: pull_request:
schedule: schedule:
- cron: "0 5 * * 1" - cron: "0 5 * * 1"
+107 -59
View File
@@ -29,7 +29,6 @@ SEVERITY_ORDER = {
"MEDIUM": 2, "MEDIUM": 2,
"LOW": 3, "LOW": 3,
"UNKNOWN": 4, "UNKNOWN": 4,
"-": 5,
"UNVERIFIED": 6, "UNVERIFIED": 6,
} }
@@ -41,7 +40,7 @@ def api(method, path, token, gitea_url, data=None):
req.add_header("Authorization", f"token {token}") req.add_header("Authorization", f"token {token}")
req.add_header("Content-Type", "application/json") req.add_header("Content-Type", "application/json")
try: try:
with urllib.request.urlopen(req) as resp: with urllib.request.urlopen(req, timeout=10) as resp:
raw = resp.read() raw = resp.read()
return json.loads(raw) if raw else None return json.loads(raw) if raw else None
except urllib.error.HTTPError as e: except urllib.error.HTTPError as e:
@@ -49,6 +48,34 @@ def api(method, path, token, gitea_url, data=None):
raise raise
def make_finding(source, id, severity, package, installed="-", fixed="-", target="-"):
return {
"source": source,
"id": id,
"severity": severity,
"package": package,
"installed": installed,
"fixed": fixed,
"target": target,
}
def cvss_score_to_severity(score):
try:
score = float(score)
except (TypeError, ValueError):
return "UNKNOWN"
if score >= 9.0:
return "CRITICAL"
if score >= 7.0:
return "HIGH"
if score >= 4.0:
return "MEDIUM"
if score > 0.0:
return "LOW"
return "UNKNOWN"
def load_trivy(path): def load_trivy(path):
findings = [] findings = []
if not path or not os.path.isfile(path): if not path or not os.path.isfile(path):
@@ -58,35 +85,31 @@ def load_trivy(path):
for result in data.get("Results", []) or []: for result in data.get("Results", []) or []:
target = result.get("Target", "?") target = result.get("Target", "?")
for vuln in result.get("Vulnerabilities", []) or []: for vuln in result.get("Vulnerabilities", []) or []:
findings.append({ findings.append(make_finding(
"source": "Trivy", "Trivy",
"id": vuln.get("VulnerabilityID", "?"), vuln.get("VulnerabilityID", "?"),
"severity": vuln.get("Severity", "UNKNOWN"), vuln.get("Severity", "UNKNOWN"),
"package": vuln.get("PkgName", "?"), vuln.get("PkgName", "?"),
"installed": vuln.get("InstalledVersion", "?"), installed=vuln.get("InstalledVersion", "?"),
"fixed": vuln.get("FixedVersion") or "-", fixed=vuln.get("FixedVersion") or "-",
"target": target, target=target,
}) ))
for misc in result.get("Misconfigurations", []) or []: for misc in result.get("Misconfigurations", []) or []:
findings.append({ findings.append(make_finding(
"source": "Trivy (Misconfig)", "Trivy (Misconfig)",
"id": misc.get("ID", "?"), misc.get("ID", "?"),
"severity": misc.get("Severity", "UNKNOWN"), misc.get("Severity", "UNKNOWN"),
"package": misc.get("Title", "?"), misc.get("Title", "?"),
"installed": "-", target=target,
"fixed": "-", ))
"target": target,
})
for secret in result.get("Secrets", []) or []: for secret in result.get("Secrets", []) or []:
findings.append({ findings.append(make_finding(
"source": "Trivy (Secret)", "Trivy (Secret)",
"id": secret.get("RuleID", "?"), secret.get("RuleID", "?"),
"severity": secret.get("Severity", "UNKNOWN"), secret.get("Severity", "UNKNOWN"),
"package": secret.get("Title", "?"), secret.get("Title", "?"),
"installed": "-", target=target,
"fixed": "-", ))
"target": target,
})
return findings return findings
@@ -101,16 +124,21 @@ def load_osv(path):
for pkg in result.get("packages", []) or []: for pkg in result.get("packages", []) or []:
info = pkg.get("package", {}) info = pkg.get("package", {})
pkg_name = f"{info.get('name', '?')} ({info.get('ecosystem', '?')})" pkg_name = f"{info.get('name', '?')} ({info.get('ecosystem', '?')})"
severity_by_id = {}
for group in pkg.get("groups", []) or []:
label = cvss_score_to_severity(group.get("max_severity"))
for vuln_id in group.get("ids", []) or []:
severity_by_id[vuln_id] = label
for vuln in pkg.get("vulnerabilities", []) or []: for vuln in pkg.get("vulnerabilities", []) or []:
findings.append({ vuln_id = vuln.get("id", "?")
"source": "OSV-Scanner", findings.append(make_finding(
"id": vuln.get("id", "?"), "OSV-Scanner",
"severity": "-", vuln_id,
"package": pkg_name, severity_by_id.get(vuln_id, "UNKNOWN"),
"installed": info.get("version", "?"), pkg_name,
"fixed": "-", installed=info.get("version", "?"),
"target": source, target=source,
}) ))
return findings return findings
@@ -128,23 +156,24 @@ def load_trufflehog(path):
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
git_meta = ((entry.get("SourceMetadata") or {}).get("Data") or {}).get("Git") or {} git_meta = ((entry.get("SourceMetadata") or {}).get("Data") or {}).get("Git") or {}
findings.append({ findings.append(make_finding(
"source": "TruffleHog", "TruffleHog",
"id": entry.get("DetectorName", "?"), entry.get("DetectorName", "?"),
"severity": "VERIFIED" if entry.get("Verified") else "UNVERIFIED", "VERIFIED" if entry.get("Verified") else "UNVERIFIED",
"package": git_meta.get("file", "?"), git_meta.get("file", "?"),
"installed": "-", target=git_meta.get("commit", "-"),
"fixed": "-", ))
"target": git_meta.get("commit", "-"),
})
return findings return findings
def sort_findings(findings):
return sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
def print_summary(findings): def print_summary(findings):
if not findings: if not findings:
print("Keine Funde.") print("Keine Funde.")
return return
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
widths = { widths = {
key: max(len(key), *(len(str(f[key])) for f in findings)) key: max(len(key), *(len(str(f[key])) for f in findings))
for key in ("source", "id", "severity", "package", "installed", "fixed", "target") for key in ("source", "id", "severity", "package", "installed", "fixed", "target")
@@ -167,8 +196,11 @@ TRUFFLEHOG_GUIDANCE = """### Vorgehen bei gefundenen Secrets
> Der Wert des Secrets selbst wird hier bewusst nicht ausgegeben, auch nicht gekürzt nur Detector, Datei und Commit. Fund lässt sich über "Ziel" (Commit-Hash) und "Paket" (Dateipfad) lokalisieren.""" > Der Wert des Secrets selbst wird hier bewusst nicht ausgegeben, auch nicht gekürzt nur Detector, Datei und Commit. Fund lässt sich über "Ziel" (Commit-Hash) und "Paket" (Dateipfad) lokalisieren."""
def escape_md_cell(value):
return str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
def build_report(findings, run_url): def build_report(findings, run_url):
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
lines = [ lines = [
"Automatisch erstellt vom Security-Scan-Workflow.", "Automatisch erstellt vom Security-Scan-Workflow.",
f"Lauf: {run_url}" if run_url else "", f"Lauf: {run_url}" if run_url else "",
@@ -177,10 +209,8 @@ def build_report(findings, run_url):
"|---|---|---|---|---|---|---|", "|---|---|---|---|---|---|---|",
] ]
for f in findings: for f in findings:
lines.append( cells = (f["source"], f["id"], f["severity"], f["package"], f["installed"], f["fixed"], f["target"])
f"| {f['source']} | {f['id']} | {f['severity']} | {f['package']} | " lines.append("| " + " | ".join(escape_md_cell(c) for c in cells) + " |")
f"{f['installed']} | {f['fixed']} | {f['target']} |"
)
if any(f["source"] == "TruffleHog" for f in findings): if any(f["source"] == "TruffleHog" for f in findings):
lines.append("") lines.append("")
lines.append(TRUFFLEHOG_GUIDANCE) lines.append(TRUFFLEHOG_GUIDANCE)
@@ -188,10 +218,15 @@ def build_report(findings, run_url):
def ensure_label(token, gitea_url, repo): def ensure_label(token, gitea_url, repo):
labels = api("GET", f"/repos/{repo}/labels?limit=50", token, gitea_url) or [] page = 1
for label in labels: while True:
if label.get("name") == LABEL_NAME: labels = api("GET", f"/repos/{repo}/labels?limit=50&page={page}", token, gitea_url) or []
return label["id"] for label in labels:
if label.get("name") == LABEL_NAME:
return label["id"]
if len(labels) < 50:
break
page += 1
created = api("POST", f"/repos/{repo}/labels", token, gitea_url, { created = api("POST", f"/repos/{repo}/labels", token, gitea_url, {
"name": LABEL_NAME, "name": LABEL_NAME,
"color": LABEL_COLOR, "color": LABEL_COLOR,
@@ -225,7 +260,7 @@ def main():
osv_exit = int(os.environ.get("OSV_EXIT", "0")) osv_exit = int(os.environ.get("OSV_EXIT", "0"))
trufflehog_exit = int(os.environ.get("TRUFFLEHOG_EXIT", "0")) trufflehog_exit = int(os.environ.get("TRUFFLEHOG_EXIT", "0"))
findings = load_trivy(trivy_path) + load_osv(osv_path) + load_trufflehog(trufflehog_path) findings = sort_findings(load_trivy(trivy_path) + load_osv(osv_path) + load_trufflehog(trufflehog_path))
print_summary(findings) print_summary(findings)
open_issue = find_open_issue(token, gitea_url, repo) open_issue = find_open_issue(token, gitea_url, repo)
@@ -251,8 +286,21 @@ def main():
else: else:
print("Keine Funde und kein offenes Issue vorhanden.") print("Keine Funde und kein offenes Issue vorhanden.")
osv_ok_exits = {0, 1}
trufflehog_ok_exits = {0, 183}
if osv_exit not in osv_ok_exits:
print(f"WARNUNG: osv-scanner beendete sich mit unerwartetem Exit-Code {osv_exit} - Scan evtl. unvollständig.", file=sys.stderr)
if trufflehog_exit not in trufflehog_ok_exits:
print(f"WARNUNG: trufflehog beendete sich mit unerwartetem Exit-Code {trufflehog_exit} - Scan evtl. unvollständig.", file=sys.stderr)
has_trivy_findings = any(f["source"].startswith("Trivy") for f in findings) has_trivy_findings = any(f["source"].startswith("Trivy") for f in findings)
if has_trivy_findings or osv_exit == 1 or trufflehog_exit == 183: if (
has_trivy_findings
or osv_exit == 1
or trufflehog_exit == 183
or osv_exit not in osv_ok_exits
or trufflehog_exit not in trufflehog_ok_exits
):
sys.exit(1) sys.exit(1)