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 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 %}
<div class="checkbox-row">
<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>
{% endif %}
<button type="submit">Scan starten</button>
@@ -315,6 +385,16 @@ a{color:var(--orange)}
</div>
{% 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="score-row">
<span class="score-num">{{ result.risk_assessment.score }}/100</span>
@@ -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,
)