BackdoorChecker / scan.js
coralexbadea's picture
Backdoor Checker v0.1.0: scanner, validation results, leaderboard
b112551 verified
Raw History Blame Contribute Delete
3.88 kB
// Backdoor Checker instant file scan (shared with SafetyCard), runs in the browser (and in Node for tests).
// A direct port of safetycard/artifacts.py: same grade rules, downloads no weights.
(function (root) {
const PICKLE_EXT = [".bin", ".pt", ".pth", ".ckpt", ".pkl", ".pickle", ".joblib"];
const NON_WEIGHT_BIN = ["training_args.bin"];
const GRADES = {
A: "Safetensors/GGUF weights only, no custom code required.",
B: "Safe weight format, but the model needs trust_remote_code (runs the author's Python).",
C: "Safe weights present, plus pickle files that can run code if loaded.",
F: "Weights only in pickle format: loading them can execute arbitrary code.",
};
function normalizeId(s) {
return (s || "").trim().replace(/^https?:\/\/huggingface\.co\//, "").replace(/\/+$/, "")
.split("/").slice(0, 2).join("/");
}
async function scanRepo(repoIdRaw, fetchFn) {
const f = fetchFn || root.fetch.bind(root);
const repoId = normalizeId(repoIdRaw);
if (!/^[\w.-]+\/[\w.-]+$/.test(repoId)) throw new Error("Use the form org/model, e.g. Qwen/Qwen2.5-1.5B-Instruct");
const r = await f(`https://huggingface.co/api/models/${repoId}?blobs=true&securityStatus=true`);
if (r.status === 401 || r.status === 404) throw new Error(`Model "${repoId}" not found, or it is private.`);
if (!r.ok) throw new Error(`Hub API error ${r.status}`);
const info = await r.json();
const sib = info.siblings || [];
const names = sib.map((s) => s.rfilename);
const lower = (n) => n.toLowerCase();
const pickles = names.filter((n) => PICKLE_EXT.some((e) => lower(n).endsWith(e)));
const safetensors = names.filter((n) => lower(n).endsWith(".safetensors"));
const gguf = names.filter((n) => lower(n).endsWith(".gguf"));
const pyFiles = names.filter((n) => n.endsWith(".py"));
let autoMap = (info.config && info.config.auto_map) || null;
let configNote = null;
if (!autoMap && names.includes("config.json")) {
try {
const c = await f(`https://huggingface.co/${repoId}/resolve/main/config.json`);
if (c.ok) autoMap = (await c.json()).auto_map || null;
else configNote = "config.json could not be read (gated model?): remote-code check incomplete.";
} catch (e) {
configNote = "config.json could not be read: remote-code check incomplete.";
}
}
const needsRemoteCode = !!autoMap;
const weightPickles = pickles.filter((p) => !NON_WEIGHT_BIN.includes(p.split("/").pop()));
const safe = safetensors.length > 0 || gguf.length > 0;
let grade;
if (safe && pickles.length === 0) grade = needsRemoteCode ? "B" : "A";
else if (safe && pickles.length > 0) grade = "C";
else if (weightPickles.length > 0) grade = "F";
else grade = needsRemoteCode ? "B" : "A";
const card = info.cardData || {};
const bm = card.base_model;
const sec = info.securityRepoStatus;
return {
repo_id: repoId,
revision: info.sha,
grade,
grade_meaning: GRADES[grade],
pickle_files: pickles,
safetensors_files: safetensors.length,
gguf_files: gguf.length,
custom_code_files: pyFiles,
needs_trust_remote_code: needsRemoteCode,
declared_base_models: bm ? (Array.isArray(bm) ? bm : [bm]) : [],
license: card.license || null,
gated: info.gated || false,
hf_security_scan: sec ? { scans_done: sec.scansDone, files_with_issues: (sec.filesWithIssues || []).map((x) => x.path) } : null,
total_size_gb: Math.round(sib.reduce((a, s) => a + (s.size || 0), 0) / 1e7) / 100,
deep_scan_supported: safetensors.length > 0 && !needsRemoteCode,
note: configNote,
};
}
root.FileScan = { scanRepo, normalizeId, GRADES };
if (typeof module !== "undefined") module.exports = root.FileScan;
})(typeof globalThis !== "undefined" ? globalThis : this);