Install
$ agentstack add skill-malkreide-mcp-audit-skill-mcp-audit-skill Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
MCP Audit — Standardisiertes Audit-Vorgehen
Dieser Skill kodiert ein reproduzierbares Audit-Verfahren für MCP-Server gegen den im Anhang dokumentierten Best-Practice-Katalog (PDF-Quelle, ~50 Checks in sieben Kategorien). Ziel: bei 30+ Servern im Portfolio dieselbe Methodik anwenden, ohne dass der menschliche Auditor (oder Claude) bei jedem Server das PDF neu interpretiert.
Das Mantra in drei Zeilen:
- Profil zuerst, Checks danach — applicability filtert alles
- Evidenz schlägt Vermutung — jeder Befund braucht Code-Stelle oder konkretes Verhalten
- Severity ohne Mitleid —
criticalblockiert Produktion, Punkt
Jeder Audit folgt sechs Schritten in dieser Reihenfolge. Abweichungen sind möglich, müssen aber im Audit-Report dokumentiert werden.
Schritt 0: Umgebung vorbereiten
Bevor irgendein Schritt beginnt, müssen Cross-Platform-Voraussetzungen erfüllt sein. Diese Sektion existiert, weil bei realen Audit-Läufen auf Windows wiederholt UTF-8- und Pfad-Probleme aufgetreten sind.
0.1 UTF-8 für Python
Auf Windows defaultet Python stdout/stderr zu cp1252 und crasht bei Emojis oder Umlauten. Vor jedem Python-Snippet:
# Bash/PowerShell — vor Python-Aufrufen exportieren:
export PYTHONUTF8=1 # Bash
$env:PYTHONUTF8 = "1" # PowerShell
Oder im Python-Code direkt:
from tools.path_utils import force_utf8_stdio
force_utf8_stdio() # idempotent, sicher mehrfach aufzurufen
0.2 Pfad-Konventionen
| Tool | Erwartetes Pfad-Format | |---|---| | Bash (cat, grep, ls) | POSIX (/c/Users/foo) | | Read / Edit / Write | OS-native (C:\Users\foo auf Windows) | | Python pathlib.Path | beides, aber konsistent halten |
Helper im Repo:
# Bash — sourceable
source tools/paths.sh
native_path=$(to_native_path "/c/Users/foo") # → C:\Users\foo auf Windows
posix_path=$(to_posix_path "C:\\Users\\foo") # → /c/Users/foo
# Python
from tools.path_utils import to_native_path, to_posix_path, is_windows
read_path = to_native_path(skill_base) # für Read-Tool-Aufrufe
0.3 Inline-Heredocs sind verboten
Inline-python3 --base-dir audits/ --catalog-dir checks/ | | Profil-Validierung (Placeholder/Schema-Gate) | python tools/validate_profile.py path/to/profile.yaml | | Catalog parsen (Frontmatter aller *.md) | python tools/parse_catalog.py --format json | | Catalog vs. Manifest validieren | python tools/parse_catalog.py --format manifest-check | | applies_when evaluieren | python tools/eval_applicability.py catalog profile.yaml | | Verification-Results aggregieren | python tools/aggregate_results.py aggregate results.json --out summary.json | | Findings-Set vs. Disk validieren | python tools/aggregate_results.py validate | | Audit-Report generieren | python tools/build_report.py | | Task-Agent-Output verifizieren | python tools/verify_raw_outputs.py raw/ --expected-ids ID1,ID2 | | Task-Agent-Run loggen | python tools/agent_run_log.py log --meta-path audit-meta.json ... | | Release-Vorschlag (Schritt 7) | python tools/propose_release.py propose | | Release anwenden (CHANGELOG + Tag) | python tools/propose_release.py apply --bump | | Tracker-Update (CSV/Notion) | python tools/tracker_sync.py update --from-summary | | Pfad zu Native/POSIX konvertieren | python tools/path_utils.py to-native |
Wenn ein Audit ein Snippet braucht das hier nicht abgedeckt ist: erst Issue im Skill-Repo öffnen, dann Helper-Script bauen, dann verwenden. Inline-Heredoc ist der Anti-Pattern, der nicht-reproduzierbare Audits erzeugt.
0.4 Run-ID + Audit-Meta initialisieren (verbindlich seit Issue #15)
Niemals date +%Y-%m-%d für den Output-Verzeichnisnamen — das hat im ersten Audit zu Drift zwischen UTC-Container und lokalem Kalendertag geführt (2026-04-30 statt 2026-05-01). Stattdessen:
# Erzeugt Output-Dir mit ISO-Timestamp + Timezone-Offset, schreibt
# initiale audit-meta.json mit Skill-Version + Catalog-Hash.
python "$SKILL_BASE/tools/audit_init.py" init "$SERVER_NAME" \
--base-dir "$TARGET/audits/" \
--skill-version "1.0.0" \
--catalog-dir "$SKILL_BASE/checks/"
# Output (JSON): { "run_id": "2026-05-02T091245-Z-srgssr-mcp", "output_dir": "...", "meta_path": "..." }
Run-ID-Format: YYYY-MM-DDTHHMMSS--, wobei ` Z (UTC) oder +HHMM/-HHMM ist. Bei Sekunden-genauer Kollision (Re-Audit unmittelbar danach) wird das Verzeichnis mit -2, -3`, ... gesuffixt; die Run-ID selbst bleibt identisch.
Die initiale audit-meta.json enthält:
server_name,run_id,started_at(ISO mit TZ-Suffix),timezone_offsetskill_version,catalog_hash(SHA-256 allerchecks/*.md+MANIFEST.txt),catalog_dir- Leeres
agent_runs-Array (wird in Step 4 vonagent_run_log.pybefüllt)
Der catalog_hash ist der Reproduzierbarkeits-Anker: jeder Re-Audit kann verifizieren, dass derselbe Katalog-Stand verwendet wurde.
Schritt 1: Profil laden
Ziel: Den Server-Kontext aus dem Notion MCP Audit Tracker (DB-ID a2736a65-677d-4cf3-9f94-e874f74a1975) holen, damit nachfolgende Schritte die richtigen Checks filtern können.
1.1 Pflichtfelder aus dem Tracker
Bevor ein Audit beginnt, müssen diese Felder in der Audit-Tracker-Karte gesetzt sein:
| Feld | Werte | Verwendung im Audit | |---|---|---| | Transport | stdio-only / dual / HTTP/SSE | filtert Netzwerk-Checks | | Auth-Modell | none / API-Key / OAuth-Proxy | filtert OAuth-Checks | | Datenklasse | Public Open Data / Verwaltungsdaten / PII | filtert PII-Checks und CH-Compliance | | Schreibzugriff | read-only / write-capable | filtert HITL-Checks | | Deployment | local-stdio / Railway / Render / andere | filtert Cloud-Checks | | Repo URL | GitHub-URL | für Code-Review-Schritte |
Wenn ein Pflichtfeld fehlt, wird der Audit gestoppt und der User aufgefordert, das Feld zu füllen. Audits mit unvollständigem Profil sind wertlos — applicability wird falsch berechnet, die Findings werden unverlässlich.
1.2 Profil-Notation für interne Verwendung
Während des Audits arbeitet Claude mit einem konsolidierten Profil-Objekt:
profile:
name: zurich-opendata-mcp
repo: https://github.com/malkreide/zurich-opendata-mcp
transport: dual
auth_model: none
data_class: Public Open Data
write_capable: false # bool — kanonisches Feld (siehe Migration unten)
deployment: [local-stdio, Railway]
is_cloud_deployed: true # derived: true iff deployment hat irgendwas ausser local-stdio (siehe Issue #16)
prio: 14 # aus Tracker-Formel
Dieses Profil ist die einzige Wahrheit für applies_when-Auswertung in Schritt 3.
Schema-Hinweis (seit Issue #13): Das kanonische Profil-Feld ist write_capable: bool. Das frühere write_access: "read-only" | "write-capable" (Enum-String) wurde abgelöst. Der Notion-Tracker behält das Schreibzugriff-Select-Feld zur besseren Lesbarkeit; audit-notion-sync.py mappt es beim pull automatisch auf write_capable: bool. Profile mit Legacy-Feld write_access führen beim Evaluator zu UnknownFieldError — das ist beabsichtigt (siehe docs/applies-when-dsl.md "loud failure"-Prinzip).
1.3 Validation-Gate (verbindlich seit Issue #14)
Bevor Step 2 startet, MUSS das Profil gegen Placeholder und Schema-Lücken geprüft werden. Im ersten realen Audit hatte der User versehentlich das Template mit ...-Werten reingepastet — Claude hat das zwar erkannt, aber nur dank Defensive-Behavior. Jetzt verbindlich:
# Profil als YAML/JSON file-validieren (oder als Inline-Block)
python "$SKILL_BASE/tools/validate_profile.py" path/to/profile.yaml
# exit 0 = clean, exit 1 = Placeholder oder Schema-Fehler
Der Validator catcht:
- Placeholder-Werte:
..., `,,TODO, leere Strings,null/None`, leere Listen, Listen mit Placeholder-Members - Fehlende Pflichtfelder: alle 15 Profil-Top-Level-Felder plus
data_source.is_swiss_open_data - Type-Mismatches:
bool-Feld mit String-Wert,list-Feld mit String-Wert, etc.
Bei Exit-1 wird Step 2 nicht gestartet. Der Output zeigt strukturiert, welche Felder betroffen sind (missing / placeholder / type_mismatch). Nutze das, um den User zur Korrektur aufzufordern.
Schritt 2: Check-Katalog laden
Ziel: Den vollständigen Katalog (checks/*.md) parsen und nach category + severity indizieren.
2.1 Sieben Kategorien
| Kategorie | Quelle | Typische Anzahl Checks | Status v0.5.0 | |---|---|---|---| | ARCH | PDF Sec 2 + Anhang A — Tool-Design, Annotations, Idempotency, Repo-Struktur, Spec-Versionierung | 10–12 | 12 / 12 ✅ | | SDK | PDF Sec 3 — FastMCP, TypeScript, Zod, Lifecycle | 5–7 | 5 / 5 ✅ | | SEC | PDF Sec 4 + Anhang B — Security (grösste Kategorie) | 20–25 | 23 / 23 ✅ | | SCALE | PDF Sec 5 — Transport, LB, Container, Gateway | 5–7 | 6 / 6 ✅ | | OBS | PDF Sec 6 + Anhang B10 — Logging, Errors, SIEM, Tracing | 5–7 | 6 / 6 ✅ | | HITL | PDF Sec 7 — Sampling, Human-in-the-Loop | 4–5 | 5 / 5 ✅ | | CH | Custom — DSG/EDÖB, Schweiz-Compliance | 5–8 | 8 / 8 ✅ | | OPS | Anhang C — Test-Strategie, Doku, Phasenarchitektur | 3–5 | 3 / 3 ✅ | | Total | | ~65 | 68 / 68 ✅ |
2.2 Severity-Stufen
| Stufe | Bedeutung | Konsequenz | |---|---|---| | critical | Sicherheitslücke oder Compliance-Bruch | Blockiert Produktion. Muss vor Release gefixt sein. | | high | Architektureller Mangel mit signifikantem Risiko | Im laufenden Sprint fixen, max. 1 Sprint Karenz. | | medium | Best-Practice-Verletzung, kein akutes Risiko | Im nächsten Sprint planen. | | low | Polish, Optimierung, Stilistik | Backlog. Bei Tippfehler-Audits: low + auto-fix. |
2.3 Check-Schema
Jeder Check ist eine eigenständige Markdown-Datei im Format:
---
id: SEC-001
title: "Confused Deputy: Per-Client Consent Flow"
category: security
severity: critical
applies_when: 'auth_model == "OAuth-Proxy"'
pdf_ref: "Sec 4.1"
evidence_required: 3
---
# Body mit Description, Verification, Pass Criteria, Remediation
Details siehe templates/finding.md und beliebige Datei in checks/.
Schritt 3: Applicability-Filter
Ziel: Aus den ~50 Checks nur diejenigen auswählen, die für das aktuelle Server-Profil tatsächlich relevant sind. Ohne diesen Filter überfluten irrelevante Findings den Report (z.B. OAuth-Checks für stdio-only-Server ohne Auth).
3.1 Auswertung der applies_when-Klausel
Die Klausel ist ein Boolean-Ausdruck gegen die Profil-Felder. Die formale DSL-Spezifikation steht in [docs/applies-when-dsl.md](docs/applies-when-dsl.md), die Referenz-Implementierung in [tools/eval_applicability.py](tools/eval_applicability.py).
| Operator | Beispiel | Bedeutung | |---|---|---| | == | transport == "HTTP/SSE" | exakter String-Vergleich | | != | auth_model != "none" | Negation | | .includes(...) | deployment.includes("Railway") | Multi-Select-Membership | | and / or | transport == "HTTP/SSE" and auth_model == "OAuth-Proxy" | Verknüpfung | | always | always | Check ist universell, läuft immer |
Pflicht: Verwende den kanonischen Evaluator, niemals Python eval() oder ad-hoc-Substitution. Letzteres hat in der Vergangenheit zu nicht-reproduzierbaren Audits geführt (Listen-vs-String-Vergleiche, True vs true, etc.).
# Catalog-Auswertung gegen ein Profil
python tools/eval_applicability.py catalog path/to/profile.yaml --format table
# Einzelner Ausdruck testen
python tools/eval_applicability.py expr 'auth_model != "none"' path/to/profile.yaml
3.2 Typische Filter-Muster
stdio-only-Server ohne Auth, Public Open Data, read-only:
- Anwendbar: alle
ARCH, alleSDK, ~5SEC(basale Best Practices),OBS-Logging-Basics, einigeCH - Nicht anwendbar: SSRF, OAuth-Flow, Session-Hijacking, Stateful-LB, Sandboxing
- Geschätzt: ~15–20 Checks
HTTP/SSE-Server mit OAuth-Proxy, Cloud-Deployment, Verwaltungsdaten:
- Anwendbar: praktisch alles
- Geschätzt: ~45–55 Checks
3.3 Applicability-Report (vor Audit-Start)
Bevor der eigentliche Audit beginnt, gibt Claude diese Übersicht aus:
=== Audit applicability for zurich-opendata-mcp ===
Profile: dual transport, no auth, Public Open Data, read-only,
Deployment: [local-stdio, Railway]
Applicable checks: 23 / 50
ARCH: 7/7 (universal)
SDK: 6/6 (universal)
SEC: 4/18 (cloud-relevant subset)
SCALE: 3/6 (Railway-relevant subset)
OBS: 3/5 (universal subset)
HITL: 0/4 (no write access, no sampling)
CH: 0/6 (Public Open Data, no PII)
Severity breakdown of applicable checks:
critical: 4 high: 11 medium: 6 low: 2
Wichtig: Wenn ein Check nicht anwendbar ist, erscheint er gar nicht im Report — nicht einmal als «N/A». Das hält Reports fokussiert und vermeidet Audit-Müdigkeit.
Schritt 4: Check-Ausführung
Ziel: Jeden anwendbaren Check methodisch verifizieren — entweder automatisch (grep, AST, curl) oder via manuellem Code-Review.
4.1 Drei Verifikationsmodi
Jeder Check definiert in seiner verification:-Sektion einen oder mehrere Modi:
| Modus | Wann | Beispiel | |---|---|---| | automated | Pattern existiert/fehlt im Repo | grep -r "expose_headers" src/ für SDK-004 | | code_review | Logische Prüfung erforderlich | OAuth-State-Single-Use bei SEC-010 | | config_check | Repo-Settings, CI, Branch-Protection | cat .github/workflows/*.yml für OBS-Checks | | runtime_test | Live-API-Verhalten testen | curl -H "X-Forwarded-For: 169.254.169.254" für SEC-004 |
4.2 Audit-Reihenfolge: Severity descending
Innerhalb der anwendbaren Checks läuft der Audit in dieser Reihenfolge:
- Alle
critical-Checks zuerst (Showstopper früh erkennen) - Dann
high - Dann
medium lowzuletzt (oder skippen falls knappe Zeit)
Wenn ein critical-Check fehlschlägt, kann der Audit nicht «pass» erhalten — egal wie gut die anderen Checks ausgehen.
4.3 Evidenz-Sammlung pro Check
Für jeden ausgeführten Check wird strukturiert dokumentiert:
check_run:
id: SEC-001
status: pass | fail | partial | skip
evidence_collected: 4 # tatsächlich beobachtet
evidence_required: 3 # Mindestmaß aus Check-Def
findings:
- "Per-client consent UI in src/oauth/consent.py:42"
- "X-Frame-Options: DENY in src/middleware/security.py:18"
- "State parameter validated single-use in src/oauth/state.py:55"
gaps:
- "Cookies nutzen __Secure- prefix statt __Host- — schwächere Subdomain-Isolation"
evaluator_notes: |
Die Implementierung ist 90% korrekt. __Host- statt __Secure-
wäre der vollständige Schutz gemäss Best Practice.
4.4 Pass-Criteria
Ein Check besteht nur dann als pass, wenn:
- Alle Pflicht-Pass-Criteria im Check erfüllt sind
- Mindestens
evidence_requiredPunkte beobachtet wurden - Keine
gapsder Severity ≥ Check-Severity vorliegen
Sonst: partial (wenn 50%+ erfüllt) oder fail.
4.5 Task-Agent-Validation-Gate (verbindlich)
Wenn die Check-Execution per Task-Agent delegiert wird (typisch bei Batch-Verarbeitung mehrerer Checks gleichzeitig), MUSS nach jedem Agent-Aufruf ein Verifikations-Gate laufen. Hintergrund: Im ersten realen Audit hat ein Task-Agent mit Done (68 tool uses · 0 tokens · 2m 20s) zurückgegeben — vollständiger stiller Fehlschlag — und der Skill hat das nicht erkannt.
# 1. Nach jedem Task-Agent-Aufruf: prüfen, dass alle erwarteten raw/-Files
# existieren UND nicht leer sind (catches the 0-token failure mode).
python "$SKILL_BASE/tools/verify_raw_outputs.py" "$OUTPUT_DIR/raw/" \
--expected-ids ARCH-001,ARCH-002,SEC-021 \
--min-bytes 1
# 2. Run-Metadata loggen — Tool-Uses, Tokens, Duration in audit-meta.json.
# Dieser Befehl exitet 1 wenn der Agent als `empty` oder `incomplete`
# klassifiziert wird.
python "$SKILL_BASE/tools/agent_run_log.py" log \
--meta-path "$OUTPUT_DIR/audit-meta.json" \
--tool-uses 73 -
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [malkreide](https://github.com/malkreide)
- **Source:** [malkreide/mcp-audit-skill](https://github.com/malkreide/mcp-audit-skill)
- **License:** MIT
- **Homepage:** https://github.com/malkreide/swiss-public-data-mcp
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.