diff --git a/.gitea/workflows/trufflehog-scan.yaml b/.gitea/workflows/trufflehog-scan.yaml index a53dcbb..d374757 100644 --- a/.gitea/workflows/trufflehog-scan.yaml +++ b/.gitea/workflows/trufflehog-scan.yaml @@ -2,10 +2,6 @@ name: TruffleHog Secret Scan on: push: - branches: - - main - - testing - - dev pull_request: schedule: - cron: "0 5 * * 1" diff --git a/scripts/report-security-issue.py b/scripts/report-security-issue.py index 75f02d1..0e6fb52 100644 --- a/scripts/report-security-issue.py +++ b/scripts/report-security-issue.py @@ -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)