Fix: Security-Scan-Report robuster gemacht & TruffleHog läuft bei jedem Push
- 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:
@@ -2,10 +2,6 @@ name: TruffleHog Secret Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- testing
|
||||
- dev
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 5 * * 1"
|
||||
|
||||
@@ -29,7 +29,6 @@ SEVERITY_ORDER = {
|
||||
"MEDIUM": 2,
|
||||
"LOW": 3,
|
||||
"UNKNOWN": 4,
|
||||
"-": 5,
|
||||
"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("Content-Type", "application/json")
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
raw = resp.read()
|
||||
return json.loads(raw) if raw else None
|
||||
except urllib.error.HTTPError as e:
|
||||
@@ -49,6 +48,34 @@ def api(method, path, token, gitea_url, data=None):
|
||||
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):
|
||||
findings = []
|
||||
if not path or not os.path.isfile(path):
|
||||
@@ -58,35 +85,31 @@ def load_trivy(path):
|
||||
for result in data.get("Results", []) or []:
|
||||
target = result.get("Target", "?")
|
||||
for vuln in result.get("Vulnerabilities", []) or []:
|
||||
findings.append({
|
||||
"source": "Trivy",
|
||||
"id": vuln.get("VulnerabilityID", "?"),
|
||||
"severity": vuln.get("Severity", "UNKNOWN"),
|
||||
"package": vuln.get("PkgName", "?"),
|
||||
"installed": vuln.get("InstalledVersion", "?"),
|
||||
"fixed": vuln.get("FixedVersion") or "-",
|
||||
"target": target,
|
||||
})
|
||||
findings.append(make_finding(
|
||||
"Trivy",
|
||||
vuln.get("VulnerabilityID", "?"),
|
||||
vuln.get("Severity", "UNKNOWN"),
|
||||
vuln.get("PkgName", "?"),
|
||||
installed=vuln.get("InstalledVersion", "?"),
|
||||
fixed=vuln.get("FixedVersion") or "-",
|
||||
target=target,
|
||||
))
|
||||
for misc in result.get("Misconfigurations", []) or []:
|
||||
findings.append({
|
||||
"source": "Trivy (Misconfig)",
|
||||
"id": misc.get("ID", "?"),
|
||||
"severity": misc.get("Severity", "UNKNOWN"),
|
||||
"package": misc.get("Title", "?"),
|
||||
"installed": "-",
|
||||
"fixed": "-",
|
||||
"target": target,
|
||||
})
|
||||
findings.append(make_finding(
|
||||
"Trivy (Misconfig)",
|
||||
misc.get("ID", "?"),
|
||||
misc.get("Severity", "UNKNOWN"),
|
||||
misc.get("Title", "?"),
|
||||
target=target,
|
||||
))
|
||||
for secret in result.get("Secrets", []) or []:
|
||||
findings.append({
|
||||
"source": "Trivy (Secret)",
|
||||
"id": secret.get("RuleID", "?"),
|
||||
"severity": secret.get("Severity", "UNKNOWN"),
|
||||
"package": secret.get("Title", "?"),
|
||||
"installed": "-",
|
||||
"fixed": "-",
|
||||
"target": target,
|
||||
})
|
||||
findings.append(make_finding(
|
||||
"Trivy (Secret)",
|
||||
secret.get("RuleID", "?"),
|
||||
secret.get("Severity", "UNKNOWN"),
|
||||
secret.get("Title", "?"),
|
||||
target=target,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
@@ -101,16 +124,21 @@ def load_osv(path):
|
||||
for pkg in result.get("packages", []) or []:
|
||||
info = pkg.get("package", {})
|
||||
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 []:
|
||||
findings.append({
|
||||
"source": "OSV-Scanner",
|
||||
"id": vuln.get("id", "?"),
|
||||
"severity": "-",
|
||||
"package": pkg_name,
|
||||
"installed": info.get("version", "?"),
|
||||
"fixed": "-",
|
||||
"target": source,
|
||||
})
|
||||
vuln_id = vuln.get("id", "?")
|
||||
findings.append(make_finding(
|
||||
"OSV-Scanner",
|
||||
vuln_id,
|
||||
severity_by_id.get(vuln_id, "UNKNOWN"),
|
||||
pkg_name,
|
||||
installed=info.get("version", "?"),
|
||||
target=source,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
@@ -128,23 +156,24 @@ def load_trufflehog(path):
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
git_meta = ((entry.get("SourceMetadata") or {}).get("Data") or {}).get("Git") or {}
|
||||
findings.append({
|
||||
"source": "TruffleHog",
|
||||
"id": entry.get("DetectorName", "?"),
|
||||
"severity": "VERIFIED" if entry.get("Verified") else "UNVERIFIED",
|
||||
"package": git_meta.get("file", "?"),
|
||||
"installed": "-",
|
||||
"fixed": "-",
|
||||
"target": git_meta.get("commit", "-"),
|
||||
})
|
||||
findings.append(make_finding(
|
||||
"TruffleHog",
|
||||
entry.get("DetectorName", "?"),
|
||||
"VERIFIED" if entry.get("Verified") else "UNVERIFIED",
|
||||
git_meta.get("file", "?"),
|
||||
target=git_meta.get("commit", "-"),
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
def sort_findings(findings):
|
||||
return sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
|
||||
|
||||
|
||||
def print_summary(findings):
|
||||
if not findings:
|
||||
print("Keine Funde.")
|
||||
return
|
||||
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
|
||||
widths = {
|
||||
key: max(len(key), *(len(str(f[key])) for f in findings))
|
||||
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."""
|
||||
|
||||
|
||||
def escape_md_cell(value):
|
||||
return str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ")
|
||||
|
||||
|
||||
def build_report(findings, run_url):
|
||||
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
|
||||
lines = [
|
||||
"Automatisch erstellt vom Security-Scan-Workflow.",
|
||||
f"Lauf: {run_url}" if run_url else "",
|
||||
@@ -177,10 +209,8 @@ def build_report(findings, run_url):
|
||||
"|---|---|---|---|---|---|---|",
|
||||
]
|
||||
for f in findings:
|
||||
lines.append(
|
||||
f"| {f['source']} | {f['id']} | {f['severity']} | {f['package']} | "
|
||||
f"{f['installed']} | {f['fixed']} | {f['target']} |"
|
||||
)
|
||||
cells = (f["source"], f["id"], f["severity"], f["package"], f["installed"], f["fixed"], f["target"])
|
||||
lines.append("| " + " | ".join(escape_md_cell(c) for c in cells) + " |")
|
||||
if any(f["source"] == "TruffleHog" for f in findings):
|
||||
lines.append("")
|
||||
lines.append(TRUFFLEHOG_GUIDANCE)
|
||||
@@ -188,10 +218,15 @@ def build_report(findings, run_url):
|
||||
|
||||
|
||||
def ensure_label(token, gitea_url, repo):
|
||||
labels = api("GET", f"/repos/{repo}/labels?limit=50", token, gitea_url) or []
|
||||
for label in labels:
|
||||
if label.get("name") == LABEL_NAME:
|
||||
return label["id"]
|
||||
page = 1
|
||||
while True:
|
||||
labels = api("GET", f"/repos/{repo}/labels?limit=50&page={page}", token, gitea_url) or []
|
||||
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, {
|
||||
"name": LABEL_NAME,
|
||||
"color": LABEL_COLOR,
|
||||
@@ -225,7 +260,7 @@ def main():
|
||||
osv_exit = int(os.environ.get("OSV_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)
|
||||
|
||||
open_issue = find_open_issue(token, gitea_url, repo)
|
||||
@@ -251,8 +286,21 @@ def main():
|
||||
else:
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user