feat(diagnostic): Sicherheit im Modell-Katalog — Anbieter, HF-Scan, Skripte im Repo
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 <noreply@anthropic.com>
This commit is contained in:
+36
-3
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user