Files
SkillSpector-Web/app.py
T

188 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
SkillSpector Web – schlanker Wrapper um die skillspector-CLI.
Sicherheitsprinzipien:
- Führt NIEMALS den gescannten Skill aus (das macht skillspector selbst auch nicht).
- Nur http/https-URLs werden als Ziel akzeptiert (kein file://, keine lokalen Pfade).
- Läuft standardmäßig mit --no-llm, damit keine Dateiinhalte nach außen gehen.
- Jeder Scan läuft in einem eigenen, danach gelöschten Temp-Verzeichnis.
- Zugriff nur mit Basic-Auth (Benutzername/Passwort aus Umgebungsvariablen).
- Harte Zeit- und Upload-Größenbegrenzung, damit ein Scan den Container nicht blockieren kann.
"""
import json
import os
import re
import shutil
import subprocess
import tempfile
from functools import wraps
from pathlib import Path
from flask import Flask, Response, render_template_string, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 50 * 1024 * 1024 # 50 MB Upload-Limit
SCAN_TMP_DIR = os.environ.get("SCAN_TMP_DIR", "/tmp/skillspector")
Path(SCAN_TMP_DIR).mkdir(parents=True, exist_ok=True)
WEBUI_USER = os.environ.get("WEBUI_USER", "admin")
WEBUI_PASSWORD = os.environ.get("WEBUI_PASSWORD") # MUSS über die Umgebung gesetzt werden
SCAN_TIMEOUT = int(os.environ.get("SCAN_TIMEOUT_SECONDS", "300"))
ALLOW_LLM = bool(os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY"))
GIT_URL_RE = re.compile(r"^https?://[^\s]+$")
def check_auth(username: str, password: str) -> bool:
return bool(WEBUI_PASSWORD) and username == WEBUI_USER and password == WEBUI_PASSWORD
def authenticate():
return Response(
"Zugriff verweigert.",
401,
{"WWW-Authenticate": 'Basic realm="SkillSpector"'},
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
if not WEBUI_PASSWORD:
return Response(
"WEBUI_PASSWORD ist nicht gesetzt – Zugriff aus Sicherheitsgründen gesperrt.",
500,
)
auth = request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated
PAGE = """
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>SkillSpector</title>
<style>
body{font-family:sans-serif;max-width:820px;margin:40px auto;padding:0 16px;background:#111;color:#eee}
input,button{width:100%;padding:9px;margin:6px 0;box-sizing:border-box;font-size:1em}
input[type=checkbox]{width:auto;display:inline}
button{background:#2ecc71;color:#000;border:none;cursor:pointer;font-weight:bold;padding:10px}
pre{background:#000;padding:12px;overflow-x:auto;white-space:pre-wrap;border-radius:6px}
.score{font-size:1.8em;font-weight:bold;margin:12px 0}
.SAFE{color:#4caf50}.CAUTION{color:#ffb300}.DO_NOT_INSTALL{color:#f44336}
label{display:block;margin-top:14px;font-size:.9em;color:#bbb}
.err{color:#f44336;font-weight:bold}
hr{border-color:#333}
</style>
</head>
<body>
<h1>🔍 SkillSpector</h1>
<p>Scannt einen Skill (Git-Repo oder ZIP) auf Sicherheitsrisiken, <strong>bevor</strong> er installiert wird.</p>
<form method="post" enctype="multipart/form-data">
<label>Git-Repository-URL (nur http/https)</label>
<input type="text" name="git_url" placeholder="https://git.obraasch.de/olli1986/mein-skill">
<label>...oder ZIP-Datei hochladen (max. 50&nbsp;MB)</label>
<input type="file" name="zip_file" accept=".zip">
{% if allow_llm %}
<label><input type="checkbox" name="use_llm"> LLM-Analyse aktivieren (sendet Dateiinhalte an den konfigurierten Provider – genauer, aber nicht mehr rein lokal)</label>
{% endif %}
<button type="submit">Scan starten</button>
</form>
<hr>
{% if result %}
<h2>Ergebnis</h2>
<p class="score {{ result.risk_assessment.recommendation }}">
{{ result.risk_assessment.score }}/100 — {{ result.risk_assessment.severity }} — {{ result.risk_assessment.recommendation }}
</p>
<pre>{{ result_json }}</pre>
{% elif error %}
<p class="err">{{ error }}</p>
{% endif %}
</body>
</html>
"""
@app.route("/", methods=["GET", "POST"])
@requires_auth
def index():
if request.method == "GET":
return render_template_string(PAGE, allow_llm=ALLOW_LLM, result=None, error=None)
use_llm = ALLOW_LLM and request.form.get("use_llm") == "on"
git_url = (request.form.get("git_url") or "").strip()
zip_file = request.files.get("zip_file")
work_dir = tempfile.mkdtemp(dir=SCAN_TMP_DIR)
try:
if zip_file and zip_file.filename:
zip_path = Path(work_dir) / "upload.zip"
zip_file.save(zip_path)
target = str(zip_path)
elif git_url:
if not GIT_URL_RE.match(git_url):
return render_template_string(
PAGE,
allow_llm=ALLOW_LLM,
result=None,
error="Nur http(s)-URLs sind erlaubt (kein file://, kein lokaler Pfad).",
)
target = git_url
else:
return render_template_string(
PAGE,
allow_llm=ALLOW_LLM,
result=None,
error="Bitte eine Git-URL angeben oder eine ZIP-Datei hochladen.",
)
report_path = Path(work_dir) / "report.json"
cmd = ["skillspector", "scan", target, "--format", "json", "-o", str(report_path)]
if not use_llm:
cmd.append("--no-llm")
proc = subprocess.run(
cmd,
cwd=work_dir,
timeout=SCAN_TIMEOUT,
capture_output=True,
text=True,
)
if report_path.exists():
result = json.loads(report_path.read_text())
return render_template_string(
PAGE,
allow_llm=ALLOW_LLM,
result=result,
result_json=json.dumps(result, indent=2, ensure_ascii=False),
error=None,
)
return render_template_string(
PAGE,
allow_llm=ALLOW_LLM,
result=None,
error=f"Scan fehlgeschlagen (Exit {proc.returncode}):\n{proc.stderr[-2000:]}",
)
except subprocess.TimeoutExpired:
return render_template_string(
PAGE,
allow_llm=ALLOW_LLM,
result=None,
error=f"Scan abgebrochen: Zeitlimit von {SCAN_TIMEOUT}s überschritten.",
)
finally:
shutil.rmtree(work_dir, ignore_errors=True)
@app.route("/healthz")
def healthz():
return {"status": "ok"}