From 0d53c9a3e2bf2d0fb4a3760b3bc9a642ca7e3b4b Mon Sep 17 00:00:00 2001 From: Oliver Braasch Date: Tue, 22 Sep 2026 12:07:20 +0200 Subject: [PATCH] Dateien nach "/" hochladen --- Dockerfile | 4 +- LICENSE | 29 +++--- README.md | 249 ++++++++++++++++++++++++++++++++------------- app.py | 129 ++++++++++++++++++++--- docker-compose.yml | 22 +++- 5 files changed, 330 insertions(+), 103 deletions(-) diff --git a/Dockerfile b/Dockerfile index b915d18..263dacc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,4 +28,6 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD python3 -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthz', timeout=3)" -CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--timeout", "310", "app:app"] +# Ein Worker mit mehreren Threads: Rate-Limit, Concurrency-Semaphore und der +# Scan-Zähler in app.py sind In-Memory-Zustand und müssen sich einen Prozess teilen. +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "1", "--threads", "4", "--timeout", "310", "app:app"] diff --git a/LICENSE b/LICENSE index 753e191..659a456 100644 --- a/LICENSE +++ b/LICENSE @@ -1,18 +1,21 @@ MIT License -Copyright (c) 2026 olli1986 +Copyright (c) 2026 Oliver Braasch -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and -associated documentation files (the "Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the -following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial -portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT -LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO -EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0937fac..9e336cb 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,186 @@ -# SkillSpector Web – Setup-Anleitung +# SkillSpector Web -Web-Oberfläche für [SkillSpector](https://github.com/NVIDIA/SkillSpector), gehärtet für -den Betrieb auf einem eigenen Server via Portainer. +Eine schlanke Web-Oberfläche für [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) – +prüft AI-Agent-Skills (Claude Code, Codex, MCP) auf Sicherheitsrisiken wie +Prompt-Injection, Daten-Exfiltration, Supply-Chain-Probleme oder gefährlichen +Code, **bevor** sie installiert werden. -## 1. Repository in Gitea anlegen +**Live:** _(hier deine Subdomain eintragen, z. B. https://skillspector.obraasch.de/)_ -1. Neues Repo z. B. unter `git.obraasch.de/olli1986/skillspector-web` anlegen. -2. Diese vier Dateien hochladen/pushen: `Dockerfile`, `requirements.txt`, `app.py`, - `docker-compose.yml`. -3. **Nicht committen:** Passwörter oder API-Keys – die kommen später als - Umgebungsvariablen in Portainer, nie ins Repo. +--- -## 2. Stack in Portainer anlegen +## Was macht das Tool? -1. **Stacks → Add stack** -2. **Build method: Repository** - - Repository URL: `https://git.obraasch.de/olli1986/skillspector-web` - - Reference: `refs/heads/main` - - Compose path: `docker-compose.yml` -3. **Environment variables** (im Portainer-UI, nicht im Repo): - - `WEBUI_USER` = z. B. `admin` - - `WEBUI_PASSWORD` = ein starkes Passwort - - optional `ANTHROPIC_API_KEY`, falls du später LLM-Analyse willst -4. **Deploy the stack** +1. Du gibst die URL eines Git-Repositorys (oder eine ZIP-Datei) mit einem + AI-Agent-Skill an. +2. Im Hintergrund läuft `skillspector scan` – reine statische Analyse + (Regex, Python-AST, YARA-Signaturen), optional ergänzt durch eine + LLM-Bewertung der Dateiinhalte. +3. Du bekommst einen Risk Score (0–100) mit Einstufung `SAFE` / `CAUTION` / + `DO_NOT_INSTALL` sowie eine Liste der gefundenen Probleme. -Ohne gesetztes `WEBUI_PASSWORD` verweigert die WebUI jeden Zugriff (Absicherung -gegen offen erreichbare Instanzen). - -## 3. Automatische Updates einrichten - -Der Trick: Im `Dockerfile` ist SkillSpector **ohne Versions-Pin** installiert -(`git+https://github.com/NVIDIA/skillspector.git`). Jeder Rebuild des Images holt -also automatisch den aktuellen `main`-Branch. Damit das regelmäßig passiert, ohne -dass du selbst etwas anfassen musst: - -1. Stack öffnen → Abschnitt **GitOps updates** -2. **Enable GitOps updates** aktivieren -3. Mechanismus: **Polling**, Intervall z. B. `24h` -4. **Force redeployment** UND **Re-pull image / rebuild** aktivieren - - → Auch wenn sich in *deinem* Repo nichts ändert, baut Portainer das Image alle - 24h neu und zieht damit automatisch die neueste SkillSpector-Version. -5. Speichern. - -Falls deine Portainer-Version "Force redeployment" beim reinen Repository-Build -nicht anbietet: alternativ einfach den Stack alle paar Wochen manuell über -**"Pull and redeploy"** neu bauen – dauert 10 Sekunden und holt ebenfalls die -neueste Version. - -## 4. Öffentlich erreichbar machen (optional) - -Der Container bindet bewusst nur an `127.0.0.1:8000`, ist also von außen gar nicht -erreichbar. Wenn du ihn z. B. unter `skillspector.obraasch.de` nutzen willst, -richte in CloudPanel einen Reverse Proxy auf `127.0.0.1:8000` ein – mit TLS -(Let's Encrypt) und idealerweise IP-Beschränkung, da die App zusätzlich zur -Basic-Auth Angriffsfläche für einen Web-Client darstellt (auch wenn sie selbst -sehr klein ist). - -## 5. Warum das deinen Server nicht gefährden kann - -- **Kein Docker-Socket-Mount** – der Container kann keine anderen Container - oder den Host steuern. -- **`read_only: true`** – das Root-Dateisystem im Container ist unveränderlich, - nur `/tmp` und das Scan-Verzeichnis sind (flüchtiger) RAM-Speicher (`tmpfs`). -- **`cap_drop: ALL`** und **`no-new-privileges`** – keine erweiterten - Linux-Rechte. -- **Ressourcenlimits** (`mem_limit`, `cpus`) – ein hängender oder böswilliger - Scan kann den restlichen Server nicht in die Knie zwingen. -- **SkillSpector führt den gescannten Skill nie aus** – nur statische Analyse - (Regex/AST/YARA) plus optionale LLM-Bewertung von Dateiinhalten. -- **`--no-llm` ist Standard** – ohne dein explizites Häkchen verlässt kein - gescannter Dateiinhalt den Container. -- Jeder Scan läuft in einem eigenen Temp-Ordner, der danach gelöscht wird. +Der gescannte Skill wird dabei **zu keinem Zeitpunkt ausgeführt**. ## Nutzung -WebUI öffnen (z. B. `http://:8000` lokal oder deine Subdomain), Basic-Auth -eingeben, Git-URL des Skills eintragen oder ZIP hochladen, "Scan starten" klicken. -Das Ergebnis zeigt Risk Score, Severity und Empfehlung (`SAFE` / `CAUTION` / -`DO_NOT_INSTALL`) sowie den vollständigen JSON-Report. +Formular öffnen → Git-URL eintragen oder ZIP hochladen → "Scan starten". +Fertig. Kein Account nötig (sofern der Betreiber den öffentlichen Modus +aktiviert hat, siehe unten). + +--- + +## Selbst betreiben: Setup mit Gitea + Portainer + +### 1. Repository klonen/forken + +Dieses Repository enthält alles, was für ein eigenständiges Deployment +nötig ist: `Dockerfile`, `requirements.txt`, `app.py`, `docker-compose.yml`. + +### 2. Stack in Portainer anlegen + +1. **Stacks → Add stack → Build method: Repository** + - Repository URL: die URL dieses Repos + - Reference: `refs/heads/main` + - Compose path: `docker-compose.yml` +2. **Environment variables** im Portainer-UI setzen (niemals ins Repo + committen): + + | Variable | Zweck | Beispiel | + |---|---|---| + | `REQUIRE_AUTH` | `true` = privat mit Login, `false` = öffentlich ohne Login | `false` | + | `WEBUI_USER` / `WEBUI_PASSWORD` | Nur relevant wenn `REQUIRE_AUTH=true` | – | + | `HOST_PORT` | Host-Port, falls der Standard (8010) belegt ist | `8011` | + | `MAX_SCANS_PER_HOUR_PER_IP` | Rate-Limit pro IP | `10` | + | `MAX_CONCURRENT_SCANS` | Wie viele Scans gleichzeitig laufen dürfen | `2` | + | `ENABLE_LLM_OPTION` | Schaltet die LLM-Checkbox für Nutzer frei | `false` | + | `OPENROUTER_API_KEY` | Nur wenn `ENABLE_LLM_OPTION=true` | `sk-or-...` | + | `OPENROUTER_MODEL` | Modell-Slug bei OpenRouter | `anthropic/claude-sonnet-4.5` | + +3. **Deploy the stack**. + +### 3. Port-Konflikt beheben ("bind: address already in use") + +Diese Fehlermeldung heißt: Ein anderer Container/Prozess auf deinem Server +belegt den Port bereits. Zwei Wege: + +- **Anderen Port wählen** (empfohlen): Umgebungsvariable `HOST_PORT` in + Portainer setzen, z. B. `8010`, `8011` … Der Compose-Datei ist das schon + vorbereitet (`127.0.0.1:${HOST_PORT:-8010}:8000`). +- **Belegenden Prozess finden**: auf dem Server `sudo ss -tlnp | grep 8000` + (oder `docker ps` – oft ist es ein anderer Stack, der ebenfalls Port 8000 + nutzt). + +### 4. Automatische Updates + +Im `Dockerfile` ist SkillSpector **ohne Versions-Pin** installiert +(`git+https://github.com/NVIDIA/skillspector.git`). Jeder Image-Rebuild holt +also automatisch die aktuellste Version. Damit das regelmäßig passiert: + +1. Stack öffnen → **GitOps updates** aktivieren +2. Mechanismus **Polling**, Intervall z. B. `24h` +3. **Force redeployment** und **Re-pull image** aktivieren + + → Portainer baut das Image alle 24 h neu, auch ohne Änderung an diesem + Repo, und zieht damit automatisch die neueste SkillSpector-Version. + +Falls deine Portainer-Version das nicht anbietet: den Stack gelegentlich +manuell über "Pull and redeploy" neu bauen. + +--- + +## LLM-Analyse über OpenRouter einrichten + +SkillSpector unterstützt LLM-Analyse über jeden OpenAI-kompatiblen Endpunkt. +[OpenRouter](https://openrouter.ai) ist genau das – daher funktioniert es mit: + +``` +SKILLSPECTOR_PROVIDER=openai +OPENAI_API_KEY= +OPENAI_BASE_URL=https://openrouter.ai/api/v1 +SKILLSPECTOR_MODEL=anthropic/claude-sonnet-4.5 # oder ein anderes Modell-Slug +``` + +Das ist im `docker-compose.yml` bereits vorbereitet (`OPENROUTER_API_KEY` / +`OPENROUTER_MODEL` setzen, `ENABLE_LLM_OPTION=true`). Zwei Dinge zu beachten: + +- **Kosten/Abuse**: Ohne `ENABLE_LLM_OPTION=true` ist die LLM-Checkbox für + Nutzer gar nicht sichtbar – dein OpenRouter-Guthaben ist also standardmäßig + nicht angreifbar, selbst im öffentlichen Modus. Das Rate-Limit + (`MAX_SCANS_PER_HOUR_PER_IP`) begrenzt zusätzlich, wie oft ein einzelner + Nutzer die LLM-Analyse überhaupt anstoßen kann. +- **Nicht jedes Modell liefert zuverlässig strukturierte JSON-Antworten.** + Falls die LLM-Analyse mit einem Fehler abbricht, zuerst ein bekanntes, + instruction-following-starkes Modell testen (z. B. ein aktuelles + Claude- oder GPT-Modell über OpenRouter); im Zweifel bleibt `--no-llm` + (Standard) immer als zuverlässiger Fallback. + +--- + +## Sicherheitsarchitektur + +**Container-Ebene** (schützt deinen Server): + +- Kein Docker-Socket-Mount – der Container kann weder andere Container noch + den Host steuern. +- `read_only: true` – Root-Dateisystem unveränderlich, nur `/tmp` und das + Scan-Verzeichnis sind flüchtiger RAM-Speicher (`tmpfs`). +- `cap_drop: ALL` und `no-new-privileges` – keine erweiterten Linux-Rechte. +- Ressourcenlimits (`mem_limit`, `cpus`) – ein hängender oder böswilliger + Scan kann den restlichen Server nicht belasten. +- Port nur auf `127.0.0.1` gebunden – öffentliche Erreichbarkeit ausschließlich + über deinen Reverse Proxy (TLS, eigene Subdomain). + +**Anwendungs-Ebene** (schützt vor Missbrauch durch Fremdnutzer, relevant seit +öffentlichem Betrieb): + +- **SSRF-Schutz**: Ziel-URLs werden aufgelöst und abgelehnt, wenn sie auf + private/interne/Loopback-Adressen zeigen (verhindert, dass jemand über das + Formular dein internes Netzwerk abscannt). +- **Rate-Limiting**: maximal `MAX_SCANS_PER_HOUR_PER_IP` Scans pro IP und + Stunde. +- **Concurrency-Limit**: maximal `MAX_CONCURRENT_SCANS` Scans gleichzeitig, + weitere Anfragen werden abgewiesen statt den Server zu überlasten. +- **Zeitlimit** je Scan (`SCAN_TIMEOUT_SECONDS`). +- **LLM-Analyse ist ein bewusstes Opt-in** des Betreibers, nicht der Nutzer. +- SkillSpector führt den gescannten Skill nie aus – nur statische Analyse + plus optionale LLM-Bewertung von Dateiinhalten. +- Jeder Scan läuft in einem eigenen, danach gelöschten Temp-Ordner. Nichts + wird dauerhaft gespeichert. + +--- + +## Rechtliches für den öffentlichen Betrieb + +Ein öffentlich erreichbares Angebot in Deutschland braucht ein Impressum +(§ 5 DDG) und eine Datenschutzerklärung. Falls du dieses Tool wie deine +anderen Werkzeuge über `software.obraasch.de` einbindest, kannst du die dort +vorhandenen Vorlagen (`impressum.html`, `datenschutz.html` aus dem +[Software-Uebersicht](https://git.obraasch.de/olli1986/Software-Uebersicht)-Repo) +weiterverwenden und um einen Hinweis ergänzen, dass bei aktivierter +LLM-Analyse Nutzereingaben an einen externen Anbieter (OpenRouter bzw. das +dort gewählte Modell) übertragen werden. Die WebUI selbst weist Nutzer direkt +im Formular darauf hin. + +Diese Angaben sind nach bestem Wissen erstellt, ersetzen aber keine +Rechtsberatung. + +--- + +## Lizenz + +Dieser Web-Wrapper (`Dockerfile`, `app.py`, `requirements.txt`, +`docker-compose.yml`, diese README) steht unter der **MIT-Lizenz**, siehe +[`LICENSE`](./LICENSE). + +Er installiert beim Bauen des Images das externe Paket **SkillSpector** +(NVIDIA, Apache License 2.0) – siehe [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md). +SkillSpector selbst ist nicht Teil dieses Repositories und bleibt vollständig +unter der Lizenz des Originalprojekts. + +## Haftungsausschluss + +Diese Software wird "wie sie ist" bereitgestellt, ohne jegliche Garantie. +Ein niedriger Risk Score ist keine Sicherheitsgarantie – SkillSpector ist ein +Hilfsmittel zur Risikoeinschätzung, keine vollständige Sicherheitsprüfung. +Für gescannte Inhalte, die von Nutzern eingereicht werden, wird keine +Verantwortung übernommen. diff --git a/app.py b/app.py index b468ef0..4a02eb6 100644 --- a/app.py +++ b/app.py @@ -1,23 +1,32 @@ """ -SkillSpector Web – schlanker Wrapper um die skillspector-CLI. +SkillSpector Web – Wrapper um die skillspector-CLI, geeignet für öffentlichen Betrieb. 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. +- Nur http/https-URLs werden akzeptiert; Ziele, die auf private/interne Adressen + auflösen (SSRF-Schutz: localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, link-local, + IPv6-Loopback/ULA), werden abgelehnt. +- Läuft standardmäßig mit --no-llm; LLM-Analyse ist nur nutzbar, wenn der Admin sie + explizit über ENABLE_LLM_OPTION freischaltet UND ein API-Key konfiguriert ist. +- Zugriff optional per Basic-Auth (REQUIRE_AUTH); bei öffentlichem Betrieb ohne Auth + greifen automatisch Rate-Limit und Concurrency-Limit. - 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. +- Harte Zeit- und Upload-Größenbegrenzung. """ +import ipaddress import json import os import re import shutil +import socket import subprocess import tempfile +import threading +import time from functools import wraps from pathlib import Path +from urllib.parse import urlparse from flask import Flask, Response, render_template_string, request @@ -27,14 +36,28 @@ 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) +REQUIRE_AUTH = os.environ.get("REQUIRE_AUTH", "true").lower() == "true" WEBUI_USER = os.environ.get("WEBUI_USER", "admin") -WEBUI_PASSWORD = os.environ.get("WEBUI_PASSWORD") # MUSS über die Umgebung gesetzt werden +WEBUI_PASSWORD = os.environ.get("WEBUI_PASSWORD") + 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")) +MAX_CONCURRENT_SCANS = int(os.environ.get("MAX_CONCURRENT_SCANS", "2")) +MAX_SCANS_PER_HOUR_PER_IP = int(os.environ.get("MAX_SCANS_PER_HOUR_PER_IP", "10")) + +LLM_KEY_PRESENT = bool(os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHROPIC_API_KEY")) +ENABLE_LLM_OPTION = os.environ.get("ENABLE_LLM_OPTION", "false").lower() == "true" +ALLOW_LLM = LLM_KEY_PRESENT and ENABLE_LLM_OPTION GIT_URL_RE = re.compile(r"^https?://[^\s]+$") +_scan_semaphore = threading.Semaphore(MAX_CONCURRENT_SCANS) +_rate_lock = threading.Lock() +_rate_log: dict[str, list[float]] = {} + +# --------------------------------------------------------------------------- # +# Auth +# --------------------------------------------------------------------------- # def check_auth(username: str, password: str) -> bool: return bool(WEBUI_PASSWORD) and username == WEBUI_USER and password == WEBUI_PASSWORD @@ -50,9 +73,12 @@ def authenticate(): def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): + if not REQUIRE_AUTH: + return f(*args, **kwargs) if not WEBUI_PASSWORD: return Response( - "WEBUI_PASSWORD ist nicht gesetzt – Zugriff aus Sicherheitsgründen gesperrt.", + "REQUIRE_AUTH=true, aber WEBUI_PASSWORD ist nicht gesetzt – " + "Zugriff aus Sicherheitsgründen gesperrt.", 500, ) auth = request.authorization @@ -63,6 +89,56 @@ def requires_auth(f): return decorated +# --------------------------------------------------------------------------- # +# Rate-Limiting (pro Client-IP, in-memory – reicht für einen einzelnen Prozess) +# --------------------------------------------------------------------------- # +def client_ip() -> str: + # Wenn hinter einem Reverse Proxy: X-Forwarded-For des Proxys vertrauen. + fwd = request.headers.get("X-Forwarded-For") + return fwd.split(",")[0].strip() if fwd else request.remote_addr or "unknown" + + +def rate_limited(ip: str) -> bool: + now = time.time() + with _rate_lock: + entries = [t for t in _rate_log.get(ip, []) if now - t < 3600] + if len(entries) >= MAX_SCANS_PER_HOUR_PER_IP: + _rate_log[ip] = entries + return True + entries.append(now) + _rate_log[ip] = entries + return False + + +# --------------------------------------------------------------------------- # +# SSRF-Schutz: nur öffentliche http(s)-Ziele erlauben +# --------------------------------------------------------------------------- # +def is_public_target(url: str) -> bool: + if not GIT_URL_RE.match(url): + return False + host = urlparse(url).hostname + if not host: + return False + if host.lower() == "localhost": + return False + try: + infos = socket.getaddrinfo(host, None) + except socket.gaierror: + return False + for info in infos: + ip = ipaddress.ip_address(info[4][0]) + if ( + ip.is_private + or ip.is_loopback + or ip.is_link_local + or ip.is_reserved + or ip.is_multicast + or ip.is_unspecified + ): + return False + return True + + PAGE = """ @@ -79,19 +155,22 @@ pre{background:#000;padding:12px;overflow-x:auto;white-space:pre-wrap;border-rad .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} +.notice{font-size:.85em;color:#999;margin-top:24px;border-top:1px solid #333;padding-top:12px} hr{border-color:#333} +a{color:#6cf}

🔍 SkillSpector

-

Scannt einen Skill (Git-Repo oder ZIP) auf Sicherheitsrisiken, bevor er installiert wird.

+

Scannt einen Skill (Git-Repo oder ZIP) auf Sicherheitsrisiken, bevor er installiert wird. +Basiert auf NVIDIA SkillSpector (Apache-2.0).

- + {% if allow_llm %} - + {% endif %}
@@ -105,6 +184,12 @@ hr{border-color:#333} {% elif error %}

{{ error }}

{% endif %} +

+ Statische Analyse läuft immer lokal. {% if allow_llm %}Bei aktivierter LLM-Analyse werden + Dateiinhalte an einen externen KI-Anbieter übertragen.{% endif %} + Der gescannte Skill wird zu keinem Zeitpunkt ausgeführt. Kein Ergebnis wird dauerhaft + gespeichert – es erscheint nur auf dieser Seite. +

""" @@ -116,10 +201,27 @@ def index(): if request.method == "GET": return render_template_string(PAGE, allow_llm=ALLOW_LLM, result=None, error=None) + ip = client_ip() + if rate_limited(ip): + return render_template_string( + PAGE, + allow_llm=ALLOW_LLM, + result=None, + error=f"Limit erreicht: maximal {MAX_SCANS_PER_HOUR_PER_IP} Scans pro Stunde und IP. Bitte später erneut versuchen.", + ) + 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") + if not _scan_semaphore.acquire(blocking=False): + return render_template_string( + PAGE, + allow_llm=ALLOW_LLM, + result=None, + error="Gerade laufen bereits zu viele Scans gleichzeitig. Bitte kurz warten und erneut versuchen.", + ) + work_dir = tempfile.mkdtemp(dir=SCAN_TMP_DIR) try: if zip_file and zip_file.filename: @@ -127,12 +229,12 @@ def index(): zip_file.save(zip_path) target = str(zip_path) elif git_url: - if not GIT_URL_RE.match(git_url): + if not is_public_target(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).", + error="Diese URL ist nicht erlaubt (nur öffentlich erreichbare http/https-Ziele).", ) target = git_url else: @@ -180,6 +282,7 @@ def index(): ) finally: shutil.rmtree(work_dir, ignore_errors=True) + _scan_semaphore.release() @app.route("/healthz") diff --git a/docker-compose.yml b/docker-compose.yml index a0b1bc5..960e568 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,16 +7,30 @@ services: restart: unless-stopped environment: + # --- Zugriff ------------------------------------------------------- + REQUIRE_AUTH: ${REQUIRE_AUTH:-true} # "false" = öffentlich ohne Login WEBUI_USER: ${WEBUI_USER:-admin} WEBUI_PASSWORD: ${WEBUI_PASSWORD} - SCAN_TIMEOUT_SECONDS: "300" - # Nur setzen, wenn LLM-Analyse gewünscht ist (optional, siehe README): - # ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} + + # --- Schutz gegen Missbrauch (greift v. a. im Public-Mode) --------- + MAX_SCANS_PER_HOUR_PER_IP: ${MAX_SCANS_PER_HOUR_PER_IP:-10} + MAX_CONCURRENT_SCANS: ${MAX_CONCURRENT_SCANS:-2} + SCAN_TIMEOUT_SECONDS: ${SCAN_TIMEOUT_SECONDS:-300} + + # --- Optionale LLM-Analyse via OpenRouter (OpenAI-kompatible API) -- + # Muss zusätzlich per ENABLE_LLM_OPTION freigeschaltet werden, damit + # öffentliche Nutzer nicht unbemerkt dein OpenRouter-Guthaben verbrauchen. + ENABLE_LLM_OPTION: ${ENABLE_LLM_OPTION:-false} + SKILLSPECTOR_PROVIDER: openai + OPENAI_API_KEY: ${OPENROUTER_API_KEY} + OPENAI_BASE_URL: https://openrouter.ai/api/v1 + SKILLSPECTOR_MODEL: ${OPENROUTER_MODEL:-anthropic/claude-sonnet-4.5} # Nur auf localhost binden – Zugriff von außen läuft über deinen # bestehenden Reverse Proxy (CloudPanel) mit TLS + eigenem Vhost. + # HOST_PORT frei wählbar, falls 8000 schon belegt ist (siehe README). ports: - - "127.0.0.1:8000:8000" + - "127.0.0.1:${HOST_PORT:-8010}:8000" # Härtung: kein Zugriff auf den Docker-Socket, kein Host-Mount, # Root-Dateisystem read-only, keine erweiterten Rechte.