CoolFace
Apppublic

NullRabbit/nr-bundle-classifier

sourceHugging Faceapache-2.0updated 5mo agoView on Hugging Face
0likes
app.py283 linesDownload Raw Back to root
1"""nr-bundle-classifier — Gradio Space for the NullRabbit bundle v1 classifier.2 3Accepts a user-uploaded bundle directory (zip or extracted), validates it4against the open bundle v1 spec (nr-bundle-spec), runs both V8 (cipher-5agnostic byte-amplification binary detector) and multiclass-folded (9-class6V8-V14+V16 unified detector) inference, and displays:7 8  - bundle metadata (corpus_id, primitive_id if labelled, fidelity_class)9  - V8 binary verdict + score10  - multiclass-folded 9-class softmax with per-class probabilities11  - scoreability + feature-coverage flags12  - any coverage warnings (e.g. pcap-sensitive misclassification risk)13 14Demonstrates the spec → corpus → model → unified-detector path end-to-end15on user-supplied data. The Space is a hosted variant of the operator-16internal demo at github.com/NullRabbitLabs/nr-substrate.17 18License: Apache-2.0. SDK: Gradio.19"""20 21from __future__ import annotations22 23import json24import shutil25import tempfile26import zipfile27from pathlib import Path28from typing import Any29 30import gradio as gr31import joblib32import numpy as np33import pyarrow.parquet as pq34from bundle_spec import BundleManifest35from huggingface_hub import hf_hub_download36 37V8_REPO = "NullRabbit/v8-cipher-agnostic"38MULTICLASS_REPO = "NullRabbit/multiclass-folded"39DATASET_REPO = "NullRabbit/nr-bundles-public"40 41 42_models_cache: dict[str, Any] = {}43 44 45def _load_models() -> tuple[dict, dict]:46    """Lazy-load both models on first inference call."""47    if "v8" not in _models_cache:48        v8_path = hf_hub_download(repo_id=V8_REPO, filename="model.joblib")49        _models_cache["v8"] = joblib.load(v8_path)50    if "multiclass" not in _models_cache:51        mc_path = hf_hub_download(repo_id=MULTICLASS_REPO, filename="model.joblib")52        _models_cache["multiclass"] = joblib.load(mc_path)53    return _models_cache["v8"], _models_cache["multiclass"]54 55 56def _modality_state(bundle_dir: Path) -> tuple[bool, int, bool]:57    responses_path = bundle_dir / "responses.parquet"58    n_resp = 059    has_resp = False60    if responses_path.is_file():61        table = pq.read_table(responses_path)62        n_resp = table.num_rows63        has_resp = n_resp > 064    has_pcap = (bundle_dir / "packets.pcap").is_file()65    return has_resp, n_resp, has_pcap66 67 68def _extract_v8_features(bundle_dir: Path) -> dict[str, float]:69    features = {n: 0.0 for n in [70        "pcap.unique_dst_ports", "pcap.unique_src_ports",71        "resp.amp_ratio_max", "resp.amp_ratio_mean", "resp.amp_ratio_median",72        "resp.req_bytes_max", "resp.resp_bytes_max",73    ]}74    rp = bundle_dir / "responses.parquet"75    if rp.is_file():76        table = pq.read_table(rp)77        if table.num_rows > 0:78            req = table.column("request_size_bytes").to_numpy()79            resp = table.column("response_size_bytes").to_numpy()80            features["resp.req_bytes_max"] = float(req.max())81            features["resp.resp_bytes_max"] = float(resp.max())82            with np.errstate(divide="ignore", invalid="ignore"):83                ratios = np.where(req > 0, resp / req, 0.0)84            features["resp.amp_ratio_max"] = float(ratios.max())85            features["resp.amp_ratio_mean"] = float(ratios.mean())86            features["resp.amp_ratio_median"] = float(np.median(ratios))87    return features88 89 90def _extract_multiclass_features(bundle_dir: Path, feature_names: list[str]) -> np.ndarray:91    """Minimal fallback feature extractor for the multi-class model.92 93    Only populates resp.* features (the rest default to 0). The model's94    OOD-by-construction behaviour on partial-coverage inputs is surfaced95    via the coverage_warning in the inference output.96    """97    features = {n: 0.0 for n in feature_names}98    rp = bundle_dir / "responses.parquet"99    if rp.is_file():100        table = pq.read_table(rp)101        if table.num_rows > 0:102            req = table.column("request_size_bytes").to_numpy()103            resp = table.column("response_size_bytes").to_numpy()104            with np.errstate(divide="ignore", invalid="ignore"):105                ratios = np.where(req > 0, resp / req, 0.0)106            for name, value in [107                ("resp.req_bytes_max", float(req.max())),108                ("resp.resp_bytes_max", float(resp.max())),109                ("resp.amp_ratio_max", float(ratios.max())),110                ("resp.amp_ratio_mean", float(ratios.mean())),111                ("resp.amp_ratio_median", float(np.median(ratios))),112            ]:113                if name in features:114                    features[name] = value115    return np.array([[features[n] for n in feature_names]], dtype=float)116 117 118def classify_bundle(uploaded_path: str | None) -> dict[str, Any]:119    """Main entrypoint. Accepts a bundle directory (zip or extracted)120    and returns a verdict dict suitable for Gradio JSON display."""121    if not uploaded_path:122        return {"error": "Please upload a bundle (.zip or extracted directory)."}123 124    upload = Path(uploaded_path)125    workdir = Path(tempfile.mkdtemp(prefix="nr-bundle-"))126 127    try:128        # Handle zip vs directory uploads.129        if upload.is_file() and upload.suffix == ".zip":130            with zipfile.ZipFile(upload, "r") as zf:131                zf.extractall(workdir)132            bundle_root = workdir133            # If the zip contains a single top-level directory, descend.134            entries = [p for p in workdir.iterdir() if p.is_dir()]135            if len(entries) == 1 and not (workdir / "manifest.json").is_file():136                bundle_root = entries[0]137        elif upload.is_dir():138            bundle_root = upload139        else:140            return {"error": "Unsupported upload: provide a .zip or directory."}141 142        mf_path = bundle_root / "manifest.json"143        if not mf_path.is_file():144            return {"error": f"No manifest.json found in upload (looked at {bundle_root})."}145 146        # Validate against bundle v1 spec.147        try:148            manifest = BundleManifest.model_validate_json(mf_path.read_text())149        except Exception as exc:150            return {151                "error": "Bundle does not validate against nr-bundle-spec v0.1.0.",152                "detail": str(exc)[:400],153            }154 155        has_resp, n_resp, has_pcap = _modality_state(bundle_root)156 157        v8_payload, mc_payload = _load_models()158 159        # V8 binary inference.160        v8_features = _extract_v8_features(bundle_root)161        X_v8 = np.array([[v8_features[n] for n in v8_payload["feature_names"]]], dtype=float)162        v8_score = float(v8_payload["model"].predict_proba(X_v8)[0, 1])163        v8_verdict = "attack" if v8_score >= 0.5 else "benign"164 165        # Multi-class inference.166        if not (has_resp or has_pcap):167            mc_block = {168                "verdict": "unscoreable",169                "reason": "No responses.parquet (with rows) and no packets.pcap present.",170            }171        else:172            X_mc = _extract_multiclass_features(bundle_root, mc_payload["feature_names"])173            proba = mc_payload["model"].predict_proba(X_mc)[0]174            class_order = mc_payload["class_order"]175            argmax = int(np.argmax(proba))176            argmax_class = class_order[argmax]177            argmax_p = float(proba[argmax])178            coverage = ("full" if has_resp and has_pcap179                        else "resp_only" if has_resp180                        else "pcap_only" if has_pcap181                        else "none")182 183            warning = None184            if coverage == "resp_only" and argmax_class != "V16" and argmax_p < 0.8:185                warning = (186                    f"argmax={argmax_class} with P={argmax_p:.3f} on resp_only "187                    "coverage; multiclass-folded was trained on full-modality "188                    "bundles. For reliable V8-V14 inference provide bundles "189                    "with raw packets.pcap present."190                )191            elif coverage == "resp_only" and argmax_class == "V16":192                warning = (193                    "argmax=V16 with resp_only coverage. V16 is load-bearing "194                    "on pcap.* features; this is likely a missing-modality "195                    "artefact, not a true gossip-abuse detection. Provide "196                    "bundles with raw packets.pcap for V16 inference."197                )198 199            mc_block = {200                "verdict": argmax_class,201                "argmax_p": round(argmax_p, 4),202                "class_probs": {c: round(float(proba[i]), 4)203                                for i, c in enumerate(class_order)},204                "feature_coverage": coverage,205                "coverage_warning": warning,206            }207 208        return {209            "bundle_manifest": {210                "corpus_id": manifest.corpus_id,211                "primitive_id": manifest.primitive_id,212                "family_id": manifest.family_id,213                "chain": manifest.chain,214                "fidelity_class": (215                    manifest.provenance.fidelity_class.value216                    if hasattr(manifest.provenance.fidelity_class, "value")217                    else str(manifest.provenance.fidelity_class)218                ),219                "ground_truth_label": (220                    manifest.ground_truth_label.value221                    if hasattr(manifest.ground_truth_label, "value")222                    else str(manifest.ground_truth_label)223                ),224            },225            "modality_state": {226                "responses_rows": n_resp,227                "packets_pcap_present": has_pcap,228            },229            "v8_binary": {230                "score": round(v8_score, 4),231                "verdict": v8_verdict,232            },233            "multiclass_folded": mc_block,234        }235    finally:236        shutil.rmtree(workdir, ignore_errors=True)237 238 239# ── Gradio interface ──────────────────────────────────────────────240 241DESCRIPTION = """242# nr-bundle-classifier243 244Run a bundle (in the open [bundle v1 format](https://github.com/NullRabbitLabs/nr-bundle-spec)) through NullRabbit's published detectors:245 246- **[V8 cipher-agnostic byte-amplification detector](https://huggingface.co/NullRabbit/v8-cipher-agnostic)** — binary attack/benign classification for byte-amplification family247- **[Multi-class softmax folded detector](https://huggingface.co/NullRabbit/multiclass-folded)** — 9-class unified detector (benign + V8/V9/V10/V11/V12/V13/V14/V16)248 249Upload a bundle directory (zip or extracted) — the Space validates against bundle v1 spec, runs both detectors, and returns per-class probabilities plus scoreability + coverage flags. Sample bundles available at [NullRabbit/nr-bundles-public](https://huggingface.co/datasets/NullRabbit/nr-bundles-public).250 251This is the data-layer artefact of NullRabbit Labs' research on **autonomous defence for decentralised networks**. The methodology is documented in the [substrate paper](https://github.com/NullRabbitLabs/nr-bundle-spec) (in preparation); the governance layer is published separately as the [earned-autonomy paper](https://doi.org/10.5281/zenodo.18406828).252 253**Note**: bundles in `nr-bundles-public` have raw `packets.pcap` dropped per the dataset's safety policy. Some class manifolds (V8/V13/V14) survive this and produce correct verdicts; others (V11, benign-with-traffic, V16) are load-bearing on pcap features and skew accordingly. Coverage warnings emit when the predicted class is sensitive to the missing modality. For reliable inference on V11/benign-with-traffic/V16, provide bundles with raw pcap retained.254"""255 256with gr.Blocks(title="nr-bundle-classifier") as demo:257    gr.Markdown(DESCRIPTION)258    with gr.Row():259        with gr.Column():260            upload = gr.File(label="Bundle (.zip or extracted dir)",261                              file_count="single")262            run_btn = gr.Button("Classify", variant="primary")263        with gr.Column():264            output = gr.JSON(label="Verdict")265 266    run_btn.click(fn=classify_bundle, inputs=upload, outputs=output)267 268    gr.Markdown("""269---270 271**Related**:272 273- [`nr-bundle-spec`](https://github.com/NullRabbitLabs/nr-bundle-spec) — open bundle v1 format (MIT)274- [`nr-bundles-public`](https://huggingface.co/datasets/NullRabbit/nr-bundles-public) — curated public sample (CC-BY-4.0)275- [`v8-cipher-agnostic`](https://huggingface.co/NullRabbit/v8-cipher-agnostic) — binary detector (Apache-2.0)276- [`multiclass-folded`](https://huggingface.co/NullRabbit/multiclass-folded) — unified detector (Apache-2.0)277- [NullRabbit Labs](https://huggingface.co/NullRabbit) · [nullrabbit.ai](https://nullrabbit.ai)278""")279 280 281if __name__ == "__main__":282    demo.launch()283