From c8e055b7d9efa3832697c61ce750120e2bee7b5c Mon Sep 17 00:00:00 2001 From: Oliver Braasch Date: Wed, 23 Sep 2026 20:50:50 +0200 Subject: [PATCH] Dateien nach "/" hochladen --- app.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++- env.example | 35 +++++++++++++++++++++ 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 env.example diff --git a/app.py b/app.py index 7eb6611..9f22017 100644 --- a/app.py +++ b/app.py @@ -24,6 +24,7 @@ import subprocess import tempfile import threading import time +import urllib.request from functools import wraps from pathlib import Path from urllib.parse import urlparse @@ -48,6 +49,12 @@ LLM_KEY_PRESENT = bool(os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHR ENABLE_LLM_OPTION = os.environ.get("ENABLE_LLM_OPTION", "false").lower() == "true" ALLOW_LLM = LLM_KEY_PRESENT and ENABLE_LLM_OPTION +# Für die deutsche Klartext-Zusammenfassung wird direkt die OpenRouter-kompatible +# Chat-API angesprochen (unabhängig von SkillSpectors eigener --llm-Option). +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") +OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://openrouter.ai/api/v1") +SUMMARY_MODEL = os.environ.get("SKILLSPECTOR_MODEL") or "openai/gpt-4o-mini" + GIT_URL_RE = re.compile(r"^https?://[^\s]+$") _scan_semaphore = threading.Semaphore(MAX_CONCURRENT_SCANS) @@ -200,6 +207,69 @@ def normalize_components(raw_components: list) -> list: return normalized +# --------------------------------------------------------------------------- # +# Deutsche Klartext-Zusammenfassung des Reports per LLM (OpenRouter-kompatibel) +# --------------------------------------------------------------------------- # +def generate_report_summary(result: dict) -> str | None: + if not OPENAI_API_KEY: + return None + + risk = result.get("risk_assessment") or {} + issues = result.get("issues") or result.get("findings") or [] + compact = { + "score": risk.get("score"), + "severity": risk.get("severity"), + "recommendation": risk.get("recommendation"), + "total_issues": len(issues), + "top_issues": [ + { + "severity": i.get("severity"), + "category": i.get("category") or i.get("type"), + "message": i.get("finding") or i.get("message") or i.get("description"), + "file": (i.get("location") or {}).get("file") or (i.get("location") or {}).get("path"), + } + for i in issues[:20] + ], + } + + prompt = ( + "Du bekommst das JSON-Ergebnis eines automatisierten Sicherheits-Scans für einen " + "AI-Agent-Skill. Schreibe eine kurze, laienverständliche Zusammenfassung auf Deutsch " + "für jemanden ohne Security-Hintergrund, als Fließtext in 2-3 kurzen Absätzen " + "(keine Überschriften, keine Code-Blöcke, keine Aufzählungszeichen):\n" + "Absatz 1: Gesamteinschätzung in 1-2 Sätzen – wie riskant ist es grob?\n" + "Absatz 2: Die wichtigsten Funde in einfachen Worten, ohne unerklärte Fachbegriffe.\n" + "Absatz 3: Konkrete Handlungsempfehlung (bedenkenlos nutzbar / mit Vorsicht " + "nutzbar / besser nicht installieren) und eine kurze Begründung warum.\n\n" + f"Scan-Ergebnis:\n{json.dumps(compact, ensure_ascii=False)}" + ) + + payload = json.dumps( + { + "model": SUMMARY_MODEL, + "messages": [{"role": "user", "content": prompt}], + "temperature": 0.3, + "max_tokens": 700, + } + ).encode("utf-8") + + req = urllib.request.Request( + f"{OPENAI_BASE_URL.rstrip('/')}/chat/completions", + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {OPENAI_API_KEY}", + }, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=60) as resp: + data = json.loads(resp.read().decode("utf-8")) + return data["choices"][0]["message"]["content"].strip() + except Exception: + return None + + # --------------------------------------------------------------------------- # # Design im Stil von software.obraasch.de: dunkler Header, orange Akzente, # helle Cards. @@ -307,7 +377,7 @@ a{color:var(--orange)} {% if allow_llm %}
- +
{% endif %} @@ -315,6 +385,16 @@ a{color:var(--orange)} {% if result %} + {% if summary_paragraphs %} +
+

Verständliche Zusammenfassung

+ {% for p in summary_paragraphs %}

{{ p }}

{% endfor %} +
+ {% elif summary_failed %} +
+

Die deutsche Zusammenfassung konnte nicht erstellt werden (KI-Anbieter nicht erreichbar oder Fehler). Der technische Bericht unten ist trotzdem vollständig.

+
+ {% endif %}
{{ result.risk_assessment.score }}/100 @@ -446,6 +526,13 @@ def index(): if report_path.exists(): result = json.loads(report_path.read_text()) raw_issues = result.get("issues") or result.get("findings") or [] + + summary_text = None + summary_failed = False + if use_llm: + summary_text = generate_report_summary(result) + summary_failed = summary_text is None + return render_template_string( PAGE, allow_llm=ALLOW_LLM, @@ -453,6 +540,8 @@ def index(): issues_by_severity=normalize_issues(raw_issues), components=normalize_components(result.get("components", [])), severity_labels=SEVERITY_LABELS_DE, + summary_paragraphs=[p for p in (summary_text or "").split("\n\n") if p.strip()], + summary_failed=summary_failed, result_json=json.dumps(result, indent=2, ensure_ascii=False), error=None, ) diff --git a/env.example b/env.example new file mode 100644 index 0000000..333a531 --- /dev/null +++ b/env.example @@ -0,0 +1,35 @@ +# Vorlage für alle Umgebungsvariablen dieses Stacks. +# NICHT als echte .env committen – dient nur der Dokumentation. +# In Arcane: Projekt öffnen -> Environment/Variables -> hier eintragen. +# (Werte hier bleiben bei jedem Git-Pull/Redeploy erhalten, sie werden +# getrennt von diesem Repo in Arcane selbst gespeichert.) + +# --- Zugriff ----------------------------------------------------------- +REQUIRE_AUTH=true +WEBUI_USER=admin +WEBUI_PASSWORD= + +# --- Netzwerk ------------------------------------------------------------ +# Host-Port, falls 8010 schon belegt ist +HOST_PORT=8010 + +# --- Schutz gegen Missbrauch -------------------------------------------- +MAX_SCANS_PER_HOUR_PER_IP=10 +MAX_CONCURRENT_SCANS=2 +SCAN_TIMEOUT_SECONDS=300 + +# --- LLM-Analyse & deutsche Zusammenfassung (optional) ------------------- +# Muss explizit auf true, sonst ist die Checkbox in der WebUI unsichtbar +# und dein OpenRouter-Guthaben ist vor fremden Nutzern geschützt. +ENABLE_LLM_OPTION=false + +# OpenRouter-Key, siehe https://openrouter.ai/keys +OPENROUTER_API_KEY= + +# Modell-Slug bei OpenRouter. Kostenlose Modelle tragen ":free" im Namen, +# z. B. "meta-llama/llama-3.3-8b-instruct:free" oder +# "deepseek/deepseek-chat-v3.1:free" - aktuelle Liste: https://openrouter.ai/models?max_price=0 +# Achtung: kleine/kostenlose Modelle liefern nicht immer zuverlässig +# strukturierte JSON-Antworten - falls die deutsche Zusammenfassung öfter +# fehlschlägt, ein etwas stärkeres (ggf. kostenpflichtiges) Modell testen. +OPENROUTER_MODEL=meta-llama/llama-3.3-8b-instruct:free