From e4963eebe2b1ef4870457d3ecc3121593412b154 Mon Sep 17 00:00:00 2001 From: Pascal Michel Date: Fri, 16 Oct 2026 04:00:04 +0200 Subject: [PATCH] Abgleich mit dem Vault-Stand 5c997a2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 4 Datei(en) geändert. Erzeugt von .vault-tools/auto-push/werkzeuge_spiegel.py. --- .claude/skills/plane-abgleich/SKILL.md | 6 ++- .vault-tools/geheimnis-scan/LIESMICH.md | 50 ++++++++++++++++++ .vault-tools/geheimnis-scan/datei_scan.py | 43 ++++++++++++++++ .vault-tools/geheimnis-scan/push_scan.py | 62 +++++++++++++++++++++++ 4 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 .vault-tools/geheimnis-scan/LIESMICH.md create mode 100644 .vault-tools/geheimnis-scan/datei_scan.py create mode 100644 .vault-tools/geheimnis-scan/push_scan.py diff --git a/.claude/skills/plane-abgleich/SKILL.md b/.claude/skills/plane-abgleich/SKILL.md index 85e1015..eac9e47 100644 --- a/.claude/skills/plane-abgleich/SKILL.md +++ b/.claude/skills/plane-abgleich/SKILL.md @@ -1,6 +1,6 @@ --- name: plane-abgleich -description: Gleicht die Checkbox-Aufgaben aus todo.md der Vault-Code-Projekte mit den Work Items der Plane-Instanz ab — in beide Richtungen, mit Konflikterkennung. Nutze diesen Skill, wenn von Plane, Work Items, Issues, Aufgabenabgleich, „todo nach Plane" oder „was ist in Plane offen" die Rede ist. +description: 'Gleicht die Checkbox-Aufgaben aus todo.md der Vault-Code-Projekte mit den Work Items der Plane-Instanz ab — in beide Richtungen, mit Konflikterkennung. Nutze diesen Skill, wenn von Plane, Work Items, Issues, Aufgabenabgleich, „todo nach Plane" oder „was ist in Plane offen" die Rede ist.' --- # Plane-Abgleich @@ -51,5 +51,7 @@ Arbeitsverzeichnis: `D:\Claude\Code Projekte\active - Plane Abgleich\skill\scrip - `--anwenden` niemals ungefragt ausführen. Es verändert Plane für alle Beteiligten. - Konflikte ohne Entscheidung bleiben liegen — das ist beabsichtigt, nicht umgehen. - Es wird nie gelöscht: keine Zeile, kein Work Item, kein Projekt. Entfernte Zeilen und - entfernte Work Items werden nur gemeldet. + entfernte Work Items werden gemeldet. Fehlt ein Work Item in Plane, nimmt `--anwenden` + den Marker aus der Zeile; die Zeile bleibt, und der nächste `--anwenden` legt dafür ein + neues Work Item an. - Projekte werden nur auf ausdrückliche Anforderung angelegt, nie nebenbei im Abgleich. diff --git a/.vault-tools/geheimnis-scan/LIESMICH.md b/.vault-tools/geheimnis-scan/LIESMICH.md new file mode 100644 index 0000000..37a4650 --- /dev/null +++ b/.vault-tools/geheimnis-scan/LIESMICH.md @@ -0,0 +1,50 @@ +--- +type: resource +title: "Geheimnis-Scan: Commits und Dateien vor dem Push durchsuchen" +status: active +summary: "Zwei Scanner suchen in ausgehenden Commits oder in einer Textdatei nach Zugangsdaten, Firmennamen, privaten IPs und Mailadressen und zeigen Geheimnis-Werte nur als Länge." +tags: [vault-tools, veroeffentlichung] +created: 2026-10-15 +updated: 2026-10-15 +--- + +# Geheimnis-Scan + +Für den Blick vor einem Push oder bevor eine Datei ein Repo verlässt. Werte, die wie Geheimnisse aussehen, stehen in der Ausgabe nur als Länge. + +| Skript | Durchsucht | Aufruf | +|---|---|---| +| `push_scan.py` | hinzugefügte Zeilen eines Commit-Bereichs | `python -I -X utf8 push_scan.py ` | +| `datei_scan.py` | eine Textdatei, etwa ein Transkript oder Export | `python -I -X utf8 datei_scan.py ` | + +## Benutzen + +```bash +# Ausgehende Commits eines Zweigs, der auf Gitea schon existiert +python -I -X utf8 push_scan.py "D:/Claude/Code Projekte/active - X" origin/main..main +# Neuer Zweig, den Gitea noch nicht kennt +python -I -X utf8 push_scan.py "D:/Claude/Code Projekte/active - X" feature/neu --not --remotes=origin +# Eine Datei vor dem Löschen oder Veröffentlichen +python -I -X utf8 datei_scan.py "D:/pfad/transkript.txt" +``` + +- `[geheim] …: 0` je Muster heißt: kein Treffer dieser Art. +- `[zaehl] private_ipv4` und `[zaehl] firma` zeigen die Werte im Klartext. Liegen sie in Fixtures oder Plänen, ist das kein Befund; die Datei sagt die Fundstelle nicht, `git grep` danach schon. +- Einen bekannten Kundennamen suchst du getrennt mit `grep -c` und gibst ihn nie aus. + +## Was die Skripte sehen und was nicht + +- **Fangnetz, kein Nachweis.** Die Muster sind reguläre Ausdrücke für Zuweisungen (`password = "…"`), private Schlüsselblöcke, Token-Präfixe (`ghp_`, `glpat-`), URLs mit Zugangsdaten und Passwort-Hashes. Ein Geheimnis ohne diese Form, etwa ein nackter Schlüssel in einer eigenen Zeile, sehen sie nicht. +- **Firmennamen** nur mit Rechtsform (`GmbH`, `AG`, `KG`, `UG`, `e.V.`). Ein Name ohne Rechtsform fällt durch. +- **`push_scan.py`** liest nur hinzugefügte Zeilen; was ein früherer Commit schon veröffentlicht hat, sieht es nicht. + +## Was belegt ist + +- 2026-10-15, vor den Pushes der Gitea-Doku-Angleichung (Welle 0): `push_scan.py` über die ausgehenden Commits von elf Repos, 0 Treffer in den Geheimnis-Mustern; der IP-Zähler schlug an Test-Fixtures an (`10.20.x.x` in Wartungsprotokoll), zählt also. +- 2026-10-15: `datei_scan.py` über ein Claude-Transkript von 341 KB: kein Treffer in den Zuweisungen, neun lange Zeichenketten, davon vier aus signierten Download-Links; Firmennamen nur Platzhalter und Arbeitgeber. +- **Offen:** Die Geheimnis-Muster sind nie gegen ein echtes oder absichtlich eingebautes Geheimnis rot gesehen worden. Bis dahin gilt „0 Treffer“ nur als Hinweis. + +## Referenzen + +- `AI/Regelwerk/veroeffentlichung.md` (im Vault): was vor der Aufnahme einer Datei geprüft wird +- `AI/Regelwerk/pruefwerkzeuge.md` (im Vault): Regressionsnachweis für Prüfskripte diff --git a/.vault-tools/geheimnis-scan/datei_scan.py b/.vault-tools/geheimnis-scan/datei_scan.py new file mode 100644 index 0000000..fca208e --- /dev/null +++ b/.vault-tools/geheimnis-scan/datei_scan.py @@ -0,0 +1,43 @@ +"""Zählt in einer Textdatei Muster für Zugangsdaten, Firmennamen, Adressen und Mailadressen. + +Ersteller: Claude (Opus 5.5) · Erstellt: 2026-10-15 · Doku: LIESMICH.md + +Gibt je Muster die Zahl der Treffer und die verschiedenen Treffer aus; Werte, die +wie Geheimnisse aussehen, nur als Länge. Aufruf: python -I -X utf8 datei_scan.py +""" +import re +import sys +from collections import Counter +from pathlib import Path + +text = Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace") + +# Muster, deren Treffer nur gezählt und als Länge gezeigt werden +GEHEIM = { + "schluesselwort_mit_wert": r"(?i)\b(pass(?:wor[dt])?|kennwort|pwd|token|secret|api[_-]?key|client[_-]?secret|bearer)\b\s*[:=]\s*['\"]?([^\s'\",;]{6,})", + "private_key_block": r"-----BEGIN [A-Z ]*PRIVATE KEY-----", + "lange_hex_oder_base64": r"\b[A-Za-z0-9+/]{40,}={0,2}\b", +} +# Muster, deren Treffer im Klartext gezeigt werden (Analyse, nicht für Ablage) +OFFEN = { + "firma": r"\b[A-ZÄÖÜ][\w&.-]*(?:\s+[A-ZÄÖÜ][\w&.-]*){0,3}\s+(?:GmbH|AG|KG|SE|e\.V\.|UG|Inc\.?|Ltd\.?)\b", + "mail": r"\b[\w.+-]+@[\w-]+(?:\.[\w-]+)+\b", + "ipv4": r"\b(?:\d{1,3}\.){3}\d{1,3}\b", + "guid": r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b", + "domain": r"\bhttps?://([\w.-]+)", + "unc_pfad": r"\\\\[\w.-]+\\[\w$.-]+", +} + +print(f"Datei: {len(text)} Zeichen, {text.count(chr(10))} Zeilen") +for name, muster in GEHEIM.items(): + treffer = re.findall(muster, text) + werte = [t[-1] if isinstance(t, tuple) else t for t in treffer] + print(f"[geheim] {name}: {len(werte)} Treffer, Längen {sorted(Counter(len(w) for w in werte).items())[:15]}") + if name == "schluesselwort_mit_wert": + for schluessel, n in Counter(t[0].lower() for t in treffer).most_common(): + print(f" Schlüssel {schluessel!r}: {n}") +for name, muster in OFFEN.items(): + zaehl = Counter(re.findall(muster, text)) + print(f"[offen] {name}: {sum(zaehl.values())} Treffer, {len(zaehl)} verschieden") + for wert, n in zaehl.most_common(40): + print(f" {n:4d} {wert}") diff --git a/.vault-tools/geheimnis-scan/push_scan.py b/.vault-tools/geheimnis-scan/push_scan.py new file mode 100644 index 0000000..e6785fe --- /dev/null +++ b/.vault-tools/geheimnis-scan/push_scan.py @@ -0,0 +1,62 @@ +"""Durchsucht die ausgehenden Commits eines Repos (Bereich a..b) nach Geheimnis-Mustern. + +Ersteller: Claude (Opus 5.5) · Erstellt: 2026-10-15 · Doku: LIESMICH.md + +Gezeigt werden nur hinzugefügte Zeilen. Werte, die wie Geheimnisse aussehen, nur als +Länge; dazu Datei und Muster. Aufruf: python -I -X utf8 push_scan.py +Bereich ohne Basis (neuer Zweig auf dem Remote): " --not --remotes=origin". +""" +import re +import subprocess +import sys +from collections import Counter, defaultdict + +repo, bereich = sys.argv[1], sys.argv[2:] +log = subprocess.run( + ["git", "-C", repo, "log", "-p", "--no-color", "--format=COMMIT %h", *bereich], + capture_output=True, text=True, encoding="utf-8", errors="replace", check=True, +).stdout + +# Muster mit Wert: nur Länge ausgeben +GEHEIM = { + "zuweisung": re.compile(r"(?i)\b(pass(?:wor[dt])?|kennwort|pwd|token|secret|api[_-]?key|client[_-]?secret|private[_-]?key)\b[\"']?\s*[:=]\s*[\"']([^\"'\s$<{]{8,})[\"']"), + "privater_schluessel": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), + "gitea_github_token": re.compile(r"\b(?:ghp_[A-Za-z0-9]{30,}|gh[ousr]_[A-Za-z0-9]{30,}|glpat-[A-Za-z0-9_-]{20,})\b"), + "url_mit_zugang": re.compile(r"https?://[^/\s:@]+:[^/\s@]+@"), + "hash_shadow": re.compile(r"\$(?:6|5|y|2[aby])\$[^\s:\"']{20,}"), +} +# Muster ohne Geheimnis: Zählung je Wert +ZAEHLEN = { + "private_ipv4": re.compile(r"\b(?:10\.\d{1,3}|192\.168|172\.(?:1[6-9]|2\d|3[01]))\.\d{1,3}\.\d{1,3}\b"), + "firma": re.compile(r"\b[A-ZÄÖÜ][\w&.-]+(?:\s+[A-ZÄÖÜ][\w&.-]+){0,2}\s+(?:GmbH|AG|KG|UG|e\.V\.)\b"), +} + +datei = "?" +commits = 0 +geheim = defaultdict(list) +zaehl = {k: Counter() for k in ZAEHLEN} +for zeile in log.splitlines(): + if zeile.startswith("COMMIT "): + commits += 1 + continue + if zeile.startswith("+++ b/"): + datei = zeile[6:] + continue + if not zeile.startswith("+") or zeile.startswith("+++"): + continue + for name, muster in GEHEIM.items(): + for m in muster.finditer(zeile): + wert = m.groups()[-1] if m.groups() else m.group(0) + geheim[name].append((datei, len(wert))) + for name, muster in ZAEHLEN.items(): + for m in muster.finditer(zeile): + zaehl[name][m.group(0)] += 1 + +print(f"Commits im Bereich: {commits}") +for name in GEHEIM: + treffer = geheim.get(name, []) + print(f"[geheim] {name}: {len(treffer)}") + for d, laenge in sorted(set(treffer))[:15]: + print(f" {d} (Wertlänge {laenge})") +for name, c in zaehl.items(): + print(f"[zaehl] {name}: {sum(c.values())} Treffer, {len(c)} verschieden: " + ", ".join(f"{w} ×{n}" for w, n in c.most_common(12)))