Feat: TruffleHog-Secret-Scan als eigene Gitea-Action hinzugefügt
- Neuer Workflow trufflehog-scan.yaml scannt die komplette Git-Historie und meldet Funde in ein eigenes Issue (Label security-scan-trufflehog), getrennt vom Trivy/OSV-Issue, aber ebenfalls über SECURITY_TOKEN - report-security-issue.py: Titel/Label per ISSUE_TITLE/ISSUE_LABEL konfigurierbar, TruffleHog-JSONL-Parsing (ohne Secret-Wert im Report), Workflow schlägt bei verifizierten Funden fehl - Rotations-Anleitung wird automatisch an Issues mit TruffleHog-Funden angehängt Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKMjR9pBxshZYZdhKyT5Ky
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
name: TruffleHog Secret Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- testing
|
||||||
|
- dev
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 5 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trufflehog-scan:
|
||||||
|
name: TruffleHog
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install TruffleHog
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o trufflehog.tar.gz \
|
||||||
|
"https://github.com/trufflesecurity/trufflehog/releases/download/v3.97.4/trufflehog_3.97.4_linux_amd64.tar.gz"
|
||||||
|
tar -xzf trufflehog.tar.gz trufflehog
|
||||||
|
chmod +x trufflehog
|
||||||
|
sudo mv trufflehog /usr/local/bin/trufflehog
|
||||||
|
rm trufflehog.tar.gz
|
||||||
|
|
||||||
|
- name: Run TruffleHog Scanner
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
trufflehog git file://. --results=verified,unknown --fail --json > trufflehog-results.json
|
||||||
|
echo "TRUFFLEHOG_EXIT=$?" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Ergebnisse & Gitea-Issue erstellen/aktualisieren
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
TOKEN: ${{ secrets.SECURITY_TOKEN }}
|
||||||
|
RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }}
|
||||||
|
ISSUE_TITLE: "Security-Scan: TruffleHog Secrets"
|
||||||
|
ISSUE_LABEL: "security-scan-trufflehog"
|
||||||
|
run: |
|
||||||
|
python3 scripts/report-security-issue.py "" "" trufflehog-results.json
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Erstellt oder kommentiert ein Gitea-Issue mit den Ergebnissen der Security-Scans.
|
"""Erstellt oder kommentiert ein Gitea-Issue mit den Ergebnissen der Security-Scans.
|
||||||
|
|
||||||
Sucht ein offenes Issue mit dem Label "security-scan". Existiert eines, wird
|
Sucht ein offenes Issue mit dem Label ISSUE_LABEL (Standard: "security-scan").
|
||||||
der aktuelle Scan-Stand als neuer Kommentar angehängt (die Historie bleibt
|
Existiert eines, wird der aktuelle Scan-Stand als neuer Kommentar angehängt
|
||||||
erhalten). Existiert keines (z.B. weil das letzte geschlossen wurde), wird ein
|
(die Historie bleibt erhalten). Existiert keines (z.B. weil das letzte
|
||||||
neues Issue erstellt. Gibt es keine Funde mehr, wird ein offenes Issue nur
|
geschlossen wurde), wird ein neues Issue erstellt. Gibt es keine Funde mehr,
|
||||||
kommentiert, nicht geschlossen.
|
wird ein offenes Issue nur kommentiert, nicht geschlossen.
|
||||||
|
|
||||||
|
Titel und Label lassen sich per Umgebungsvariable ISSUE_TITLE / ISSUE_LABEL
|
||||||
|
überschreiben, damit z.B. TruffleHog-Funde in ein eigenes Issue laufen statt
|
||||||
|
in das gemeinsame Trivy/OSV-Issue.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -14,11 +18,20 @@ import sys
|
|||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
LABEL_NAME = "security-scan"
|
LABEL_NAME = os.environ.get("ISSUE_LABEL", "security-scan")
|
||||||
LABEL_COLOR = "#b60205"
|
LABEL_COLOR = "#b60205"
|
||||||
ISSUE_TITLE = "Security-Scan: Offene Schwachstellen"
|
ISSUE_TITLE = os.environ.get("ISSUE_TITLE", "Security-Scan: Offene Schwachstellen")
|
||||||
|
|
||||||
SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "UNKNOWN": 4, "-": 5}
|
SEVERITY_ORDER = {
|
||||||
|
"VERIFIED": -1,
|
||||||
|
"CRITICAL": 0,
|
||||||
|
"HIGH": 1,
|
||||||
|
"MEDIUM": 2,
|
||||||
|
"LOW": 3,
|
||||||
|
"UNKNOWN": 4,
|
||||||
|
"-": 5,
|
||||||
|
"UNVERIFIED": 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def api(method, path, token, gitea_url, data=None):
|
def api(method, path, token, gitea_url, data=None):
|
||||||
@@ -101,6 +114,32 @@ def load_osv(path):
|
|||||||
return findings
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def load_trufflehog(path):
|
||||||
|
findings = []
|
||||||
|
if not path or not os.path.isfile(path):
|
||||||
|
return findings
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
entry = json.loads(line)
|
||||||
|
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", "-"),
|
||||||
|
})
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
def print_summary(findings):
|
def print_summary(findings):
|
||||||
if not findings:
|
if not findings:
|
||||||
print("Keine Funde.")
|
print("Keine Funde.")
|
||||||
@@ -118,6 +157,16 @@ def print_summary(findings):
|
|||||||
print(row_fmt.format(*(str(f[k]) for k in header)))
|
print(row_fmt.format(*(str(f[k]) for k in header)))
|
||||||
|
|
||||||
|
|
||||||
|
TRUFFLEHOG_GUIDANCE = """### Vorgehen bei gefundenen Secrets
|
||||||
|
|
||||||
|
1. **Sofort rotieren/widerrufen**: Das betroffene Secret (Token, Passwort, Schlüssel) beim jeweiligen Dienst ungültig machen und durch ein neues ersetzen. Ein einmal committetes Secret gilt als kompromittiert, auch wenn es später aus der Historie entfernt wird.
|
||||||
|
2. **Ursache beheben**: Neues Secret nur noch über Umgebungsvariablen/Secrets-Store einbinden, nicht erneut hart codieren.
|
||||||
|
3. **Historie bereinigen (optional, manuell, erst nach Schritt 1)**: Mit `git filter-repo` oder BFG Repo-Cleaner den Commit-Inhalt entfernen, danach `git push --force` in Absprache mit allen Mitwirkenden – bestehende Clones/Forks werden dadurch ungültig.
|
||||||
|
4. **Issue schließen**, sobald rotiert wurde. TruffleHog findet das alte Secret ggf. weiterhin in der Historie – nach der Rotation ist das unkritisch.
|
||||||
|
|
||||||
|
> 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 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"]))
|
findings = sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"]))
|
||||||
lines = [
|
lines = [
|
||||||
@@ -132,6 +181,9 @@ def build_report(findings, run_url):
|
|||||||
f"| {f['source']} | {f['id']} | {f['severity']} | {f['package']} | "
|
f"| {f['source']} | {f['id']} | {f['severity']} | {f['package']} | "
|
||||||
f"{f['installed']} | {f['fixed']} | {f['target']} |"
|
f"{f['installed']} | {f['fixed']} | {f['target']} |"
|
||||||
)
|
)
|
||||||
|
if any(f["source"] == "TruffleHog" for f in findings):
|
||||||
|
lines.append("")
|
||||||
|
lines.append(TRUFFLEHOG_GUIDANCE)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -164,14 +216,16 @@ def find_open_issue(token, gitea_url, repo):
|
|||||||
def main():
|
def main():
|
||||||
trivy_path = sys.argv[1] if len(sys.argv) > 1 else None
|
trivy_path = sys.argv[1] if len(sys.argv) > 1 else None
|
||||||
osv_path = sys.argv[2] if len(sys.argv) > 2 else None
|
osv_path = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
trufflehog_path = sys.argv[3] if len(sys.argv) > 3 else None
|
||||||
|
|
||||||
gitea_url = os.environ["GITEA_URL"].rstrip("/")
|
gitea_url = os.environ["GITEA_URL"].rstrip("/")
|
||||||
repo = os.environ["REPO"]
|
repo = os.environ["REPO"]
|
||||||
token = os.environ["TOKEN"]
|
token = os.environ["TOKEN"]
|
||||||
run_url = os.environ.get("RUN_URL", "")
|
run_url = os.environ.get("RUN_URL", "")
|
||||||
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"))
|
||||||
|
|
||||||
findings = load_trivy(trivy_path) + load_osv(osv_path)
|
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)
|
||||||
@@ -198,7 +252,7 @@ def main():
|
|||||||
print("Keine Funde und kein offenes Issue vorhanden.")
|
print("Keine Funde und kein offenes Issue vorhanden.")
|
||||||
|
|
||||||
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:
|
if has_trivy_findings or osv_exit == 1 or trufflehog_exit == 183:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user