From 237a216945cad3eb2f0383c55aa797431a584bdf Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 31 Oct 2026 21:15:48 +0100 Subject: [PATCH] =?UTF-8?q?feat(diagnostic):=20Sicherheit=20im=20Modell-Ka?= =?UTF-8?q?talog=20=E2=80=94=20Anbieter,=20HF-Scan,=20Skripte=20im=20Repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Box laedt nur die GGUF-Datei (llama.cpp -hf / paralleler Download per sha256); Skripte, Pickle u.ae. im selben Repo kommen nie auf die Box. Liegen sie dort, ist das trotzdem ein Warnsignal fuer den Anbieter. - hfRepoSafety: HF-Dateiliste mit securityFileStatus (ProtectAI, AV, Pickle-Import, VirusTotal) → Befunde (unsafe/suspicious/caution …), Anzahl geprueft-sicher, riskante Dateien (.py/.sh/.exe/.pkl/.pt …), bekannter Anbieter (Hersteller + bekannte GGUF-Umwandler). - Katalog: "⚠ Befund" / "⚠ Skripte" / "✓" je Eintrag; beim Hinzufuegen sofort, Bestand beim naechsten Aktualisieren. - ℹ: Abschnitt "Sicherheit". - Laden/Nur downloaden: bei Befunden erst Rueckfrage. Getestet gegen HuggingFace: unsloth/mradermacher bekannt und ohne Befund; Virtue-AI-HUB/VulnLLM-R-7B (Original) mit Befund "caution" an training_args.bin (Pickle mit Imports) — das GGUF von mradermacher ist sauber. Co-Authored-By: Claude Opus 5.5 --- diagnostic/index.html | 23 ++++++++++++++++++++++- diagnostic/server.js | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 2cb4991..198622e 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -9830,6 +9830,9 @@ const searchText = [m.id, m.description, m.hfRepo, m.quant].filter(Boolean).join(' '); return '' + '' + escapeHtml(m.id) + '' + + (m.safety && m.safety.flagged && m.safety.flagged.length ? ' ⚠ Befund' + : m.safety && m.safety.risky && m.safety.risky.length ? ' ⚠ Skripte' : '') + + (m.safety && m.safety.known ? ' ✓' : '') + (m.tools ? ' 🔧' : '') + (m.uncensored ? ' unzensiert' : '') + (m.added_at && (Date.now() - new Date(m.added_at).getTime()) < 7 * 864e5 ? ' neu' : '') @@ -10279,6 +10282,7 @@
Kurzbeschreibung wird vom lokalen Modell aus der Modellkarte erstellt …
${fam} + ${row('Sicherheit', _modelSafetyHtml(j.safety))} ${row('Werkzeuge', j.tools === true ? '🔧 ja — die Chat-Vorlage kennt Werkzeug-Aufrufe' : j.tools === false ? 'nein — eher für Übersetzen, Zusammenfassen, Erklären' : 'unbekannt (keine Chat-Vorlage)')} ${row('Größe', m.sizeGB ? '~' + m.sizeGB + ' GB · ' + escapeHtml(m.quant || '') : '')} ${row('Grundmodell', j.base ? escapeHtml(j.base) : '')} @@ -10290,6 +10294,15 @@
${j.intro ? `
Aus der Modellkarte (${escapeHtml(j.introFrom)}, englisch)
${escapeHtml(j.intro)}
` : ''}`); } + function _modelSafetyHtml(sf) { + if (!sf) return 'unbekannt'; + const parts = [sf.known ? '✓ bekannter Anbieter' : 'unbekannter Anbieter — eher vorsichtig']; + parts.push(sf.flagged.length + ? `⚠ HuggingFace meldet Befunde: ${sf.flagged.map(f => escapeHtml(f.path) + ' (' + escapeHtml(f.status) + ')').join(', ')}` + : `HuggingFace-Scan: ${sf.scanned} Datei(en) geprüft, keine Befunde`); + if (sf.risky.length) parts.push(`Im Repo liegen Skripte/ausführbare Dateien: ${sf.risky.map(escapeHtml).join(', ')}. Die Box lädt nur die GGUF-Datei — diese Dateien kommen nie auf die Box. Aber: Warnsignal für den Anbieter.`); + return parts.join('
'); + } // Deutsche Kurzbeschreibung: einmal vom lokalen Modell erzeugt, danach aus dem Katalog async function _loadModelSummary(repo) { let j; @@ -10551,9 +10564,10 @@ } // Nur auf die Platte der Box (der aus der Ziel-Karten-Auswahl) — in keine Karte - function downloadModelOnly(i) { + async function downloadModelOnly(i) { const m = _llmCatalog[i]; if (!m) return; + if (!await _safetyGate(m)) return; const sel = document.getElementById('llm-cat-box-' + i); const target = String((sel && sel.value) || '').split('|')[0]; if (!target) return; @@ -10568,9 +10582,16 @@ openLlmDownloads(); } + async function _safetyGate(m) { + const sf = m && m.safety; + if (!sf || !sf.flagged || !sf.flagged.length) return true; + return await uiConfirm(`Vorsicht bei ${m.id}\n\nHuggingFace meldet bei Dateien in diesem Repo Befunde (${sf.flagged.map(f => f.path).join(', ')}). ` + + 'Die Box lädt zwar nur die GGUF-Datei, aber wer Verdächtiges hochlädt, dem ist auch das Modell nicht zu trauen. Trotzdem laden?'); + } async function provisionModel(i) { const m = _llmCatalog[i]; if (!m) return; + if (!await _safetyGate(m)) return; const sel = document.getElementById('llm-cat-box-' + i); const [target, gpu] = String((sel && sel.value) || '').split('|'); if (!target) return; diff --git a/diagnostic/server.js b/diagnostic/server.js index bbe372a..b021709 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -573,10 +573,13 @@ async function refreshLlmCatalogFromHF() { e.quant = q.quant; e.sizeGB = q.sizeGB; e.quants = q.quants; delete e.unusable; })); } - // Werkzeug-Faehigkeit nachtragen (alle Eintraege, die noch keine Angabe haben) - const noTools = [...byId.values()].filter(e => e.hfRepo && e.tools === undefined); + // Werkzeug-Faehigkeit + Sicherheits-Scan nachtragen (wo noch keine Angabe) + const noTools = [...byId.values()].filter(e => e.hfRepo && (e.tools === undefined || e.safety === undefined)); for (let i = 0; i < noTools.length; i += 6) { - await Promise.all(noTools.slice(i, i + 6).map(async e => { e.tools = await hfToolSupport(e.hfRepo); })); + await Promise.all(noTools.slice(i, i + 6).map(async e => { + if (e.tools === undefined) e.tools = await hfToolSupport(e.hfRepo); + if (e.safety === undefined) e.safety = await hfRepoSafety(e.hfRepo); + })); } let added = 0; for (const e of byId.values()) { if (e._new) { added++; delete e._new; } } @@ -827,6 +830,7 @@ async function llmModelInfo(repo) { tags: tags.slice(0, 20), families, uncensored: !!unc, tools: await hfToolSupport(repo), + safety: await hfRepoSafety(repo), uncensoredWhy: unc ? `erkannt an „${unc[0]}“ in Name/Schlagworten` : "", intro: await hfReadmeIntro(base || repo) || await hfReadmeIntro(repo), introFrom: base || repo, @@ -847,6 +851,34 @@ async function hfToolSupport(repo) { return tpl ? false : null; // keine Vorlage = unbekannt } +// ── Sicherheit eines Repos ───────────────────────────────────────── +// Die Box laedt NUR die GGUF-Datei (llama.cpp -hf / paralleler Download per +// sha256) — Skripte, Pickle o.ae. daneben werden nie geholt oder ausgefuehrt. +// Liegen sie trotzdem im Repo, ist das ein Warnsignal fuer den Anbieter. +// HuggingFace scannt jede Datei (securityFileStatus) — das Ergebnis zeigen wir. +const EXTRA_KNOWN_GGUF_AUTHORS = new Set(["mradermacher", "thebloke", "maziyarpanahi", "nousresearch", + "dphn", "cognitivecomputations", "fdtn-ai", "quantfactory", "lmstudio-ai"]); +function knownGgufAuthor(a) { // Funktion: TRUSTED_GGUF_AUTHORS steht weiter unten in der Datei + return TRUSTED_GGUF_AUTHORS.has(a) || EXTRA_KNOWN_GGUF_AUTHORS.has(a); +} +const RISKY_FILE_RE = /\.(py|sh|bash|bat|cmd|ps1|exe|dll|so|dylib|pkl|pickle|pt|pth|ckpt|joblib|npy|zip|tar|gz|7z|jar|js|vbs)$|(^|\/)pytorch_model[^/]*\.bin$/i; +async function hfRepoSafety(repo) { + const author = String(repo).split("/")[0].toLowerCase(); + const out = { known: knownGgufAuthor(author), flagged: [], risky: [], scanned: 0 }; + const tree = await hfJson(`https://huggingface.co/api/models/${repo}/tree/main?recursive=1&expand=true`); + for (const f of Array.isArray(tree) ? tree : []) { + if (f.type !== "file") continue; + const st = f.securityFileStatus || {}; + const subs = Object.values(st).filter(v => v && typeof v === "object").map(v => String(v.status || "")); + const all = [String(st.status || ""), ...subs].map(x => x.toLowerCase()); + if (all.some(x => /unsafe|suspicious|malicious|caution|infected/.test(x))) out.flagged.push({ path: f.path, status: all.filter(Boolean).join(", ") }); + else if (all.includes("safe")) out.scanned++; + if (RISKY_FILE_RE.test(f.path)) out.risky.push(f.path); + } + out.risky = out.risky.slice(0, 20); + return out; +} + async function addHfRepoToCatalog(repo, description = "") { repo = hfRepoFromInput(repo); if (!/^[\w.-]+\/[\w.-]+$/.test(String(repo || ""))) throw new Error("ungueltiges Repo — Link oder org/name"); @@ -864,6 +896,7 @@ async function addHfRepoToCatalog(repo, description = "") { cat.push({ id, hfRepo: repo, quant: q.quant, ctx: 8192, sizeGB: q.sizeGB, quants: q.quants, description: description || "Aus den HuggingFace-Trends übernommen", source: "hf", tools: await hfToolSupport(repo), uncensored: UNCENSORED_RE.test(repo), + safety: await hfRepoSafety(repo), added_at: new Date().toISOString().slice(0, 10) }); saveLlmCatalog(cat); return { models: cat, id, existed: false };