Dateien nach "/" hochladen

This commit is contained in:
Oliver Braasch committed 2026-09-23 20:50:50 +02:00
1 parent 929c3f8c95
commit c8e055b7d9
2 files changed
+125 -1

No files matched your search

+90 -1
View File
@@ -24,6 +24,7 @@ import subprocess
import tempfile import tempfile
import threading import threading
import time import time
import urllib.request
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse 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" ENABLE_LLM_OPTION = os.environ.get("ENABLE_LLM_OPTION", "false").lower() == "true"
ALLOW_LLM = LLM_KEY_PRESENT and ENABLE_LLM_OPTION 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]+$") GIT_URL_RE = re.compile(r"^https?://[^\s]+$")
_scan_semaphore = threading.Semaphore(MAX_CONCURRENT_SCANS) _scan_semaphore = threading.Semaphore(MAX_CONCURRENT_SCANS)
@@ -200,6 +207,69 @@ def normalize_components(raw_components: list) -> list:
return normalized 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, # Design im Stil von software.obraasch.de: dunkler Header, orange Akzente,
# helle Cards. # helle Cards.
@@ -307,7 +377,7 @@ a{color:var(--orange)}
{% if allow_llm %} {% if allow_llm %}
<div class="checkbox-row"> <div class="checkbox-row">
<input type="checkbox" name="use_llm" id="use_llm"> <input type="checkbox" name="use_llm" id="use_llm">
<label for="use_llm">LLM-Analyse aktivieren (sendet Dateiinhalte an den konfigurierten KI-Anbieter)</label> <label for="use_llm">Ausführlichen Bericht mit deutschen Erklärungen erstellen (nutzt KI über den konfigurierten Anbieter, sendet Dateiinhalte extern)</label>
</div> </div>
{% endif %} {% endif %}
<button type="submit">Scan starten</button> <button type="submit">Scan starten</button>
@@ -315,6 +385,16 @@ a{color:var(--orange)}
</div> </div>
{% if result %} {% if result %}
{% if summary_paragraphs %}
<div class="card">
<h3 class="sev-heading">Verständliche Zusammenfassung</h3>
{% for p in summary_paragraphs %}<p>{{ p }}</p>{% endfor %}
</div>
{% elif summary_failed %}
<div class="card">
<p class="notice">Die deutsche Zusammenfassung konnte nicht erstellt werden (KI-Anbieter nicht erreichbar oder Fehler). Der technische Bericht unten ist trotzdem vollständig.</p>
</div>
{% endif %}
<div class="card"> <div class="card">
<div class="score-row"> <div class="score-row">
<span class="score-num">{{ result.risk_assessment.score }}/100</span> <span class="score-num">{{ result.risk_assessment.score }}/100</span>
@@ -446,6 +526,13 @@ def index():
if report_path.exists(): if report_path.exists():
result = json.loads(report_path.read_text()) result = json.loads(report_path.read_text())
raw_issues = result.get("issues") or result.get("findings") or [] 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( return render_template_string(
PAGE, PAGE,
allow_llm=ALLOW_LLM, allow_llm=ALLOW_LLM,
@@ -453,6 +540,8 @@ def index():
issues_by_severity=normalize_issues(raw_issues), issues_by_severity=normalize_issues(raw_issues),
components=normalize_components(result.get("components", [])), components=normalize_components(result.get("components", [])),
severity_labels=SEVERITY_LABELS_DE, 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), result_json=json.dumps(result, indent=2, ensure_ascii=False),
error=None, error=None,
) )
+35
View File
@@ -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