From c294336058771a7fa5009df0e4c5ec87eb63592d Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 3 May 2026 14:00:55 +0200 Subject: [PATCH] fix: 26 kritische + 29 wichtige Sicherheits- und Robustheitsprobleme behoben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - serve.py: URL-Encoding fix, Body-Limit (10MB), Port-Reuse, TOCTOU-Race, Logging - index.html: XSS in renderTemplates geheilt, Escape-Key für Edit-Modal, Accessibility - templates.json: Pfade ohne ../, leere Descriptions ergänzt - validate.py: categories/ entfernt, CLI-Flags umbenannt, zu mutually_exclusive_group - smoke_test.sh: set -euo pipefail, Port-Validation, Timeout 5s, code-Fallback - cleanup_server.sh: lsof statt pgrep, Graceful-Term + SIGKILL-Fallback - agent_verify.sh: set -euo pipefail, ROOT-Pfade, dynamischer Port, grep-Crash fix - AGENTS.md: history/ entfernt, Pfad-Schema präzisiert - README.md: categories/ entfernt, Web-Ansicht + API-Endpunkte hinzugefügt --- AGENTS.md | 10 +-- README.md | 33 +++++++-- scripts/agent_verify.sh | 34 +++++---- scripts/cleanup_server.sh | 28 ++++++-- scripts/smoke_test.sh | 11 ++- scripts/validate.py | 135 +++++++++++++++++++++-------------- web/index.html | 54 ++++++++------ web/serve.py | 144 +++++++++++++++++++++++--------------- web/templates.json | 14 ++-- 9 files changed, 289 insertions(+), 174 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0155f73..942302a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,8 +69,8 @@ Ein Commit = eine logische Änderung. Commit-Message im Stil der bestehenden His ### Einstieg ```bash -python3 web/serve.py # startet Server auf http://localhost:8081 -./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4) +python3 web/serve.py # startet Server auf http://localhost:8081 (Server-Port) +./scripts/smoke_test.sh # temporärer Server + Endpunkt-Check + Cleanup (fuer R4, Standard-Port 8082) ./scripts/cleanup_server.sh # beendet hängende Instanzen python3 scripts/validate.py # validiert Template-Struktur ``` @@ -85,12 +85,14 @@ python3 scripts/validate.py # validiert Template-Struktur | `templates/system/*.json` | System-Templates | | `templates/user/*.md` | User-Templates | | `scripts/validate.py` | Template-Validierung | +| `scripts/cleanup_server.sh` | Beendet serve.py auf Port 8081 (Standard) oder angegebenem Port | | `docs/` | Weitere technische Dokumentation | -| `history/CHANGELOG.md` | Änderungs-Chronik | ### Datenschema `templates.json` und Template-Einträge -`web/templates.json` ist eine **Liste** von Einträgen. Schema jedes Eintrags: +`web/templates.json` ist eine **Liste** von Einträgen. `path`-Felder sind **relativ zu ROOT** (Projektstamm), OHNE `../` Präfix. + +Schema jedes Eintrags: ```json { "path": "…", "type": "…", "name": "…", "description": "…", "version": "…", "tags": [], "format": "md|json" } diff --git a/README.md b/README.md index 5c8871c..792bc68 100644 --- a/README.md +++ b/README.md @@ -13,11 +13,6 @@ prompt_template/ │ ├── user/ # Benutzer-Prompts (Emails, Texte, etc.) │ └── custom/ # Benutzerdefinierte Templates │ -├── categories/ # Optional: Kategorisierte Templates -│ ├── marketing/ -│ ├── technical/ -│ └── creative/ -│ ├── scripts/ # Hilfsskripte │ └── validate.py # Template-Validierung │ @@ -27,8 +22,33 @@ prompt_template/ --- +## Web-Ansicht + +Der Server startet mit: +```bash +python3 web/serve.py # http://localhost:8081 +``` + +### API-Endpunkte + +| URL | Methode | Beschreibung | +|-----|---------|--------------| +| `/` | GET | Frontend (index.html) | +| `/templates.json` | GET | Katalog (aus web/templates.json) | +| `/templates/system/*.json` | GET | System-Templates | +| `/templates/user/*.md` | GET | User-Templates | +| `/templates/...` | PUT | Template speichern (Content-Type: text/plain) | + +### Validierung +```bash +python3 scripts/validate.py --all +``` + ## Dateiformate +#### Einzelnes Template (JSON) +Ein Template-File (z.B. `templates/system/code_reviewer.json`) hat dieses Schema: + ### JSON (empfohlen für strukturierte Templates) ```json { @@ -45,6 +65,9 @@ prompt_template/ } ``` +#### Katalog (web/templates.json) +Der Katalog ist eine Liste von Einträgen: + ### Markdown (für einfache Templates mit Dokumentation) ```markdown # Template Name diff --git a/scripts/agent_verify.sh b/scripts/agent_verify.sh index 27621ab..507db61 100755 --- a/scripts/agent_verify.sh +++ b/scripts/agent_verify.sh @@ -2,16 +2,17 @@ # Verifikations-Hook fuer dual_agent-Pipeline. # Wird nach dem Executor-Lauf aufgerufen; Exit 0 = gruen. # stdout/stderr landen bei Retry als Kontext beim Planner. -set -u +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" FAIL=0 # --- 1. Statisch: JS-Syntax in index.html ------------------------------ -if [ -f web/index.html ]; then +if [ -f "$ROOT/web/index.html" ]; then python3 - <<'PY' || FAIL=1 import re, subprocess, tempfile, sys -html = open('web/index.html').read() +html = open(sys.argv[1] if len(sys.argv) > 1 else 'web/index.html').read() for i, s in enumerate(re.findall(r']*>([\s\S]*?)', html)): - with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False) as f: + with tempfile.NamedTemporaryFile('w', suffix='.js', delete=True) as f: f.write(s); p = f.name r = subprocess.run(['node', '--check', p], capture_output=True, text=True) if r.returncode != 0: @@ -19,41 +20,44 @@ for i, s in enumerate(re.findall(r']*>([\s\S]*?)', html)): sys.exit(1) print('JS syntax OK') PY +else + echo "FAIL: web/index.html fehlt" + FAIL=1 fi # --- 2. Dynamisch: Smoke-Test (alle Endpunkte) ------------------------- -if [ -x scripts/smoke_test.sh ]; then - ./scripts/smoke_test.sh 8088 || FAIL=1 +if [ -x "$ROOT/scripts/smoke_test.sh" ]; then + SMOKE_PORT=$(( $$ % 1000 + 9000 )) + "$ROOT/scripts/smoke_test.sh" "$SMOKE_PORT" || FAIL=1 fi # --- 3. Semantisch: Pseudo-Fix-Muster verbieten ------------------------ # Diese Patterns haben wir als No-Op-Fixes im JSON-Editor erlebt. # Ein neuer Bugfix darf sie nicht wieder einfuehren. -if [ -f web/index.html ]; then - # No-Op-Pattern: leerer Container + while(firstChild) -> nichts zu bewegen - if grep -qE 'document\.createElement\([^)]+\)[^;]*;[[:space:]]*[^;]*\.appendChild\([^)]*firstChild\)' web/index.html; then - : # zu grob — deaktiviert - fi +if [ -f "$ROOT/web/index.html" ]; then # Doppeldeklaration einer Funktion for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do - count=$(grep -cE "function[[:space:]]+${fn}\\b" web/index.html || true) + count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || echo 0) if [ "$count" -gt 1 ]; then echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)" FAIL=1 fi done # Platzhalter-Leichen in geaenderten HTML-Attributen - if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' web/index.html; then + if grep -qE 'an[[:space:]][[:space:]]+und[[:space:]][[:space:]]+angehaengt' "$ROOT/web/index.html"; then echo "FAIL: Platzhalter-Leichen in der Datei (Variablen nicht interpoliert)" FAIL=1 fi fi # --- 4. Git-Hygiene: Commit-Message ohne Platzhalter-Leichen ---------- +if ! git rev-parse HEAD >/dev/null 2>&1; then + echo "WARN: Kein Commit im Repo" +fi last_msg=$(git log -1 --pretty=%B 2>/dev/null || true) -if echo "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then +if printf '%s\n' "$last_msg" | grep -qE '\b(an|in|mit)[[:space:]][[:space:]]+und'; then echo "FAIL: letzter Commit hat doppelte Leerzeichen (Platzhalter nicht ersetzt):" - echo "$last_msg" | head -3 + printf '%s\n' "$last_msg" | head -3 FAIL=1 fi diff --git a/scripts/cleanup_server.sh b/scripts/cleanup_server.sh index 7c6ba3e..c297931 100755 --- a/scripts/cleanup_server.sh +++ b/scripts/cleanup_server.sh @@ -1,10 +1,26 @@ -#!/bin/bash -# Beendet laufende Instanzen von web/serve.py, um Port-Konflikte auf 8081 zu lösen. +#!/usr/bin/env bash set -euo pipefail -if pgrep -f "python3 .*web/serve.py" >/dev/null; then - pkill -f "python3 .*web/serve.py" - echo "Alte serve.py-Prozesse beendet." +PORT="${1:-8081}" + +# Finde Prozess auf Port +PID=$(lsof -ti ":$PORT" 2>/dev/null || true) + +if [ -n "$PID" ]; then + # Graceful shutdown attempt + kill -TERM "$PID" 2>/dev/null || true + # Warte bis zu 5 Sekunden + for i in $(seq 1 10); do + if ! kill -0 "$PID" 2>/dev/null; then + break + fi + sleep 0.5 + done + # Force kill if still running + if kill -0 "$PID" 2>/dev/null; then + kill -9 "$PID" 2>/dev/null || true + fi + echo "Prozess $PID auf Port $PORT beendet." else - echo "Keine laufende serve.py-Instanz gefunden." + echo "Keine laufende Instanz auf Port $PORT gefunden." fi diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index f17d920..e209901 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -7,12 +7,15 @@ # ./scripts/smoke_test.sh 9000 # anderer Port # Exit-Code: 0 = alle Endpunkte 200, sonst 1. -set -u +set -euo pipefail PORT="${1:-8082}" +[[ "$PORT" =~ ^[0-9]+$ ]] || { echo "Usage: $0 [port]" >&2; exit 1; } ROOT="$(cd "$(dirname "$0")/.." && pwd)" LOG="/tmp/smoke.$PORT.log" EXIT_CODE=1 MARKER="SMOKE_TEST_SERVER_$PORT" +TIMEOUT=30 +START_TIME=$(date +%s) # Vorherige Instanzen dieses Smoke-Tests beenden pkill -f "$MARKER" 2>/dev/null || true @@ -55,7 +58,10 @@ trap cleanup EXIT INT TERM # 127.0.0.1 statt localhost umgeht den Resolver — sonst kann unter # WSL2/IPv6 ein TCP-SYN auf einen unbenutzten Port minutenlang ohne # RST hängen. --connect-timeout 1 ist Defense-in-Depth. -for i in $(seq 1 20); do +for i in $(seq 1 50); do + if (( $(date +%s) - START_TIME >= TIMEOUT )); then + break + fi if curl -sf --connect-timeout 1 -o /dev/null "http://127.0.0.1:$PORT/"; then break; fi sleep 0.1 done @@ -78,6 +84,7 @@ for p in "/" "/index.html" "/templates.json" \ "/templates/user/email_draft.md" \ "/templates/user/brainstorming.md"; do code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p") + code="${code:-000}" status="ok" [ "$code" = "200" ] || { status="FAIL"; FAIL=1; } printf '%-50s %s %s\n' "$p" "$code" "$status" diff --git a/scripts/validate.py b/scripts/validate.py index 9b8de9f..f1bb55d 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -29,7 +29,10 @@ JSON_SCHEMA = { r"^.+$": { "type": "object", "properties": { - "type": {"type": "string", "enum": ["string", "number", "enum", "boolean"]}, + "type": { + "type": "string", + "enum": ["string", "number", "enum", "boolean"], + }, "required": {"type": "boolean"}, "default": {}, "description": {"type": "string"}, @@ -46,30 +49,29 @@ JSON_SCHEMA = { # Muster für Markdown-Templates MD_REQUIRED_SECTIONS = ["Template", "Variablen"] -MD_VARIABLE_PATTERN = re.compile(r"\|\s*(\w+)\s*\|\s*(\w+)\s*\|") def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: """Validiert ein JSON-Template.""" errors = [] - + try: - with open(filepath, 'r', encoding='utf-8') as f: + with open(filepath, "r", encoding="utf-8") as f: data = json.load(f) except json.JSONDecodeError as e: return False, [f"❌ JSON Syntax Error: {e}"] except Exception as e: return False, [f"❌ Datei kann nicht gelesen werden: {e}"] - + # Schemas validieren if not isinstance(data, dict): return False, ["❌ Root muss ein Object sein"] - + # Required Felder for field in JSON_SCHEMA.get("required", []): if field not in data: errors.append(f"❌ Fehlendes Pflichtfeld: '{field}'") - + # Feld-Typen validieren for field, schema in JSON_SCHEMA.get("properties", {}).items(): if field in data: @@ -80,12 +82,14 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: errors.append(f"❌ Feld '{field}' muss ein Object sein") elif field_type == "array" and not isinstance(data[field], list): errors.append(f"❌ Feld '{field}' muss ein Array sein") - + # Pattern validieren if "pattern" in schema and isinstance(data[field], str): if not re.match(schema["pattern"], data[field]): - errors.append(f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}") - + errors.append( + f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}" + ) + # Template prüfen if "template" in data: template = data["template"] @@ -98,8 +102,10 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: defined_vars = set(data["variables"].keys()) undefined_vars = template_vars - defined_vars if undefined_vars: - errors.append(f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}") - + errors.append( + f"❌ Undefinierte Variablen im Template: {', '.join(undefined_vars)}" + ) + # Variablen validieren if "variables" in data: variables = data["variables"] @@ -113,57 +119,68 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: if "type" not in var_schema: errors.append(f"❌ Variable '{var_name}' benötigt ein 'type' Feld") if var_schema.get("type") == "enum" and "values" not in var_schema: - errors.append(f"❌ Enum Variable '{var_name}' benötigt 'values' Array") - + errors.append( + f"❌ Enum Variable '{var_name}' benötigt 'values' Array" + ) + return len(errors) == 0, errors def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]: """Validiert ein Markdown-Template.""" errors = [] - + try: - with open(filepath, 'r', encoding='utf-8') as f: + with open(filepath, "r", encoding="utf-8") as f: content = f.read() except Exception as e: return False, [f"❌ Datei kann nicht gelesen werden: {e}"] - + # Mindestlänge if len(content.strip()) < 50: - errors.append("❌ Template zu kurz (mind. 50 Zeichen)") - + errors.append("⚠️ Warnung: Template zu kurz (mind. 50 Zeichen)") + # Titel prüfen if not content.startswith("# "): errors.append("❌ Fehlender Titel (erwartet: # Titel)") - + # Pflichtabschnitte prüfen for section in MD_REQUIRED_SECTIONS: if f"## {section}" not in content and f"# {section}" not in content: errors.append(f"❌ Fehlender Abschnitt: {section}") - + # Variablen-Tabelle prüfen if "Variablen" in content: var_section_start = content.find("## Variablen") if var_section_start == -1: var_section_start = content.find("# Variablen") - + if var_section_start != -1: - var_section = content[var_section_start:var_section_start + 500] + var_section = content[var_section_start : var_section_start + 500] if "| Variable |" not in var_section: errors.append("❌ Variablen-Tabelle nicht im korrekten Format") - + # Template-Block prüfen if "Template" in content: - template_start = content.find("```") - if template_start == -1: + template_block_start = content.find("```") + if template_block_start != -1: + # Finde den Code-Block nach "## Template" oder "# Template" + template_section = content.find("## Template", content.find("Template")) + if template_section == -1: + template_section = content.find("# Template", content.find("Template")) + if template_section > template_block_start: + template_block_start = template_section + if template_block_start == -1: errors.append("❌ Kein Code-Block für Template gefunden") else: # Prüfe ob Variablen im Template sind - template_content = content[template_start:] + template_content = content[template_block_start:] if "{" not in template_content or "}" not in template_content: - errors.append("⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden") - - return len(errors) == 0, errors + errors.append( + "⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden" + ) + + return len([e for e in errors if e.startswith("❌")]) == 0, errors def validate_template(filepath: Path) -> Tuple[bool, List[str]]: @@ -176,10 +193,17 @@ def validate_template(filepath: Path) -> Tuple[bool, List[str]]: return False, [f"❌ Unsupported file type: {filepath.suffix}"] +ALLOWED_DIRS = {"templates/system", "templates/user", "templates/custom", "categories"} + + def find_templates(directory: Path) -> List[Path]: """Findet alle Template-Dateien in einem Verzeichnis Baum.""" templates = [] - for root, _, files in os.walk(directory): + for root, dirs, files in os.walk(directory): + rel = os.path.relpath(root, directory) + if rel != "." and rel.split(os.sep)[0] not in ALLOWED_DIRS: + dirs.clear() # Skip nicht-erlaubte Subdirs + continue for file in files: if file.endswith((".json", ".md")): templates.append(Path(root) / file) @@ -195,35 +219,38 @@ Beispiele: python validate.py templates/system/code_reviewer.json python validate.py --all python validate.py --json templates/ - """ + """, ) parser.add_argument("path", nargs="?", help="Pfad zum Template oder Verzeichnis") parser.add_argument("--all", action="store_true", help="Alle Templates validieren") - parser.add_argument("--json", action="store_true", help="Nur JSON-Templates validieren") - parser.add_argument("--md", action="store_true", help="Nur Markdown-Templates validieren") - + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( + "--type-json", action="store_true", help="Nur JSON-Templates validieren" + ) + type_group.add_argument( + "--type-md", action="store_true", help="Nur Markdown-Templates validieren" + ) + args = parser.parse_args() - + base_dir = Path(__file__).parent.parent - + if args.all: # Alle Templates finden templates = find_templates(base_dir / "templates") - if not args.json: - templates += find_templates(base_dir / "categories") - - if args.json: + + if args.type_json: templates = [t for t in templates if t.suffix == ".json"] - if args.md: + if args.type_md: templates = [t for t in templates if t.suffix == ".md"] - + elif args.path: path = Path(args.path) if path.is_dir(): templates = find_templates(path) - if args.json: + if args.type_json: templates = [t for t in templates if t.suffix == ".json"] - if args.md: + if args.type_md: templates = [t for t in templates if t.suffix == ".md"] else: templates = [path] @@ -231,19 +258,19 @@ Beispiele: print("❌ Bitte Pfad angeben oder --all verwenden") print("Beispiel: python validate.py --all") sys.exit(1) - + if not templates: print("❌ Keine Templates gefunden") sys.exit(1) - + # Validierung total = len(templates) valid = 0 invalid = 0 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print(f"Validiere {total} Template(s)...\n") - + for template_path in sorted(templates): is_valid, errors = validate_template(template_path) # Make path relative to base_dir, handling both absolute and relative paths @@ -252,7 +279,7 @@ Beispiele: except ValueError: # If template_path is not under base_dir, use absolute path rel_path = str(template_path) - + if is_valid: print(f"✅ {rel_path}") valid += 1 @@ -261,10 +288,10 @@ Beispiele: for error in errors: print(f" {error}") invalid += 1 - - print(f"\n{'='*60}") + + print(f"\n{'=' * 60}") print(f"Ergebnis: {valid} ✅ | {invalid} ❌ | {total} Total") - + sys.exit(0 if invalid == 0 else 1) diff --git a/web/index.html b/web/index.html index fdb0adc..2b50626 100644 --- a/web/index.html +++ b/web/index.html @@ -171,7 +171,7 @@ #edit-content-content input[type="checkbox"] + label, #edit-modal input[type="checkbox"] + label { - color: #222222; + color: #e0e0e0; font-weight: normal; } @@ -331,7 +331,7 @@ .template-item .tag { background: var(--bg-input); - color: var(--text-muted); + color: #b0b0b0; padding: 3px 8px; border-radius: 4px; font-size: 11px; @@ -451,7 +451,7 @@ -