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:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- testing
|
|
||||||
- dev
|
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 5 * * 1"
|
- cron: "0 5 * * 1"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user