simonmorley commited on
Commit
4c35f56
·
verified ·
1 Parent(s): ffc98cc

nr-bundle-classifier — initial release (V8 binary + multiclass-folded, 2026-05-13)

Browse files
Files changed (5) hide show
  1. LICENSE +191 -0
  2. README.md +71 -8
  3. app.py +282 -0
  4. release-cert.json +137 -0
  5. requirements.txt +7 -0
LICENSE ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of tracking or improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for describing the origin of the Work and
141
+ reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Support. While redistributing the Work or
166
+ Derivative Works thereof, You may accept upon your own behalf the
167
+ responsibility to provide, and accept charging a fee for, accepting
168
+ warranty, support, indemnity, or other liability obligations and/or
169
+ rights consistent with this License. However, in accepting such
170
+ obligations, You may act only on Your own behalf and on Your sole
171
+ responsibility, not on behalf of any other Contributor, and only
172
+ if You agree to indemnify, defend, and hold each Contributor
173
+ harmless for any liability incurred by, or claims asserted against,
174
+ such Contributor by reason of your accepting any such warranty
175
+ or support.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ Copyright 2026 NullRabbit Labs Ltd
180
+
181
+ Licensed under the Apache License, Version 2.0 (the "License");
182
+ you may not use this file except in compliance with the License.
183
+ You may obtain a copy of the License at
184
+
185
+ http://www.apache.org/licenses/LICENSE-2.0
186
+
187
+ Unless required by applicable law or agreed to in writing, software
188
+ distributed under the License is distributed on an "AS IS" BASIS,
189
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
190
+ implied. See the License for the specific language governing
191
+ permissions and limitations under the License.
README.md CHANGED
@@ -1,13 +1,76 @@
1
  ---
2
- title: Nr Bundle Classifier
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: blue
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: nr-bundle-classifier
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: gradio
7
+ sdk_version: 4.44.0
 
8
  app_file: app.py
9
+ license: apache-2.0
10
+ short_description: Bundle v1 classifier — V8 + multi-class folded
11
+ tags:
12
+ - cybersecurity
13
+ - blockchain
14
+ - network-security
15
+ - validator-security
16
+ - sui
17
+ - solana
18
  ---
19
 
20
+ # nr-bundle-classifier
21
+
22
+ Interactive Gradio Space for NullRabbit's published bundle v1 classifiers. Accepts a user-uploaded bundle (zip or directory), validates against the open [bundle v1 spec](https://github.com/NullRabbitLabs/nr-bundle-spec), and runs both the V8 cipher-agnostic byte-amplification binary detector and the multi-class softmax folded 9-class unified detector. Returns per-class probabilities + scoreability + feature-coverage flags.
23
+
24
+ This is the data-layer artefact of NullRabbit Labs' research on **autonomous defence for decentralised networks**. The methodology is the contribution; the Space is a worked demonstration of the spec → corpus → model → unified-detector path end-to-end on user-supplied data.
25
+
26
+ ## What it shows
27
+
28
+ For each uploaded bundle, the Space displays:
29
+
30
+ 1. **Bundle metadata** parsed from `manifest.json` (corpus_id, primitive_id, family, chain, fidelity_class, ground_truth_label).
31
+ 2. **Modality state** (`responses_rows`, `packets_pcap_present`).
32
+ 3. **V8 binary verdict** — attack/benign + calibrated P(attack).
33
+ 4. **Multi-class folded verdict** — 9-class softmax (benign + V8/V9/V10/V11/V12/V13/V14/V16) with per-class P + feature_coverage flag + coverage_warning when the predicted class is sensitive to missing modalities.
34
+
35
+ ## How to try it
36
+
37
+ - Upload a bundle directory or `.zip` of one. Sample bundles are available at [NullRabbit/nr-bundles-public](https://huggingface.co/datasets/NullRabbit/nr-bundles-public) (CC-BY-4.0).
38
+ - Quickest path: download one bundle from the public dataset (e.g. `crp_1ef98f1fc0644369`, a `sui_F14` compute-amp attack) and upload the directory zipped.
39
+
40
+ ## Backing models
41
+
42
+ - **V8 cipher-agnostic byte-amplification detector** (Apache-2.0): [NullRabbit/v8-cipher-agnostic](https://huggingface.co/NullRabbit/v8-cipher-agnostic). Binary classifier over 7 cipher-agnostic features. Reference detector for byte-amplification attacks against validator JSON-RPC.
43
+ - **Multi-class softmax folded detector** (Apache-2.0): [NullRabbit/multiclass-folded](https://huggingface.co/NullRabbit/multiclass-folded). 9-class joint classifier over 107 features. Unified detector for the V8-V14 + V16 attack-family taxonomy.
44
+
45
+ Both models are products of NullRabbit's pre-registration discipline applied to network-layer attack detection. The iterative leak-surface peeling pattern is documented in their model cards.
46
+
47
+ ## Honest limitations
48
+
49
+ - **Public dataset bundles have raw `packets.pcap` dropped** per their safety policy. Some class manifolds (V8 response_amp, V13 service_misconfig, V14 compute_amp) survive this and produce correct verdicts; others (V11 rate_limiter_bypass, benign-with-traffic, V16 gossip-abuse) are load-bearing on `pcap.*` features and skew accordingly. Coverage warnings emit when the predicted class is sensitive to the missing modality.
50
+ - **n=1 OOF fragility** on the V16 load-bearing benign (SOL_BG01). Documented in the multiclass-folded model card. The fitted model routes SOL_BG01 to benign correctly; OOF held-out is fragile. Production V16 deployment requires corpus scale-up.
51
+ - **No streaming detection**: this Space scores single bundles, not live packet streams. Production deployment uses IBSR (an eBPF-based extractor) feeding the same models in a real-time loop; that's the operator-side runbook, not this Space.
52
+
53
+ ## Methodology
54
+
55
+ NullRabbit's training cycles follow pre-registration discipline. Each detector cycle has a design document committed before the trainer runs. Audits run on close against sanity floors, per-feature ablation trails, and falsification holdouts. Where an audit fires, training halts, the design is re-registered, and the prior version is retracted in writing.
56
+
57
+ The **iterative leak-surface peeling pattern** is the methodology contribution. The current model cycle (V16 → multi-class folded v2, 2026-05-13) is the worked example at the unified-detector layer: V15 binary pre-registered a leak caveat (manifest may learn protocol shape, not attack shape); cycle2 corpus expansion provided the load-bearing UDP benign that made the caveat empirically testable; V15 evaluation confirmed the caveat; V16 binary retrained with corpus augmentation closed the caveat at the n=1 fragile level; multi-class folded v2 absorbed V16 into the unified detector with the load-bearing benign test passing at training-set scale and the OOF fragility surfaced honestly.
58
+
59
+ The corpus format and family taxonomy are open at `nr-bundle-spec`. The methodology is open (in preparation as the substrate paper). The specific corpus contents beyond `nr-bundles-public` are proprietary.
60
+
61
+ ## Related
62
+
63
+ - **Bundle format spec**: [`nr-bundle-spec`](https://github.com/NullRabbitLabs/nr-bundle-spec) (MIT)
64
+ - **Reference public bundles**: [NullRabbit/nr-bundles-public](https://huggingface.co/datasets/NullRabbit/nr-bundles-public) (CC-BY-4.0)
65
+ - **V8 binary detector**: [NullRabbit/v8-cipher-agnostic](https://huggingface.co/NullRabbit/v8-cipher-agnostic) (Apache-2.0)
66
+ - **Multi-class folded detector**: [NullRabbit/multiclass-folded](https://huggingface.co/NullRabbit/multiclass-folded) (Apache-2.0)
67
+ - **Earned-autonomy paper** (governance layer): [Zenodo DOI 10.5281/zenodo.18406828](https://doi.org/10.5281/zenodo.18406828)
68
+ - **Substrate paper** (data-layer methodology, in preparation)
69
+ - **NullRabbit Labs**: [huggingface.co/NullRabbit](https://huggingface.co/NullRabbit)
70
+ - **Website**: [nullrabbit.ai](https://nullrabbit.ai)
71
+
72
+ ## Contact
73
+
74
+ Research enquiries: simon@nullrabbit.ai
75
+
76
+ Spec compliance or format questions — open an issue at [`nr-bundle-spec`](https://github.com/NullRabbitLabs/nr-bundle-spec).
app.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """nr-bundle-classifier — Gradio Space for the NullRabbit bundle v1 classifier.
2
+
3
+ Accepts a user-uploaded bundle directory (zip or extracted), validates it
4
+ against the open bundle v1 spec (nr-bundle-spec), runs both V8 (cipher-
5
+ agnostic byte-amplification binary detector) and multiclass-folded (9-class
6
+ V8-V14+V16 unified detector) inference, and displays:
7
+
8
+ - bundle metadata (corpus_id, primitive_id if labelled, fidelity_class)
9
+ - V8 binary verdict + score
10
+ - multiclass-folded 9-class softmax with per-class probabilities
11
+ - scoreability + feature-coverage flags
12
+ - any coverage warnings (e.g. pcap-sensitive misclassification risk)
13
+
14
+ Demonstrates the spec → corpus → model → unified-detector path end-to-end
15
+ on user-supplied data. The Space is a hosted variant of the operator-
16
+ internal demo at github.com/NullRabbitLabs/nr-substrate.
17
+
18
+ License: Apache-2.0. SDK: Gradio.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import shutil
25
+ import tempfile
26
+ import zipfile
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ import gradio as gr
31
+ import joblib
32
+ import numpy as np
33
+ import pyarrow.parquet as pq
34
+ from bundle_spec import BundleManifest
35
+ from huggingface_hub import hf_hub_download
36
+
37
+ V8_REPO = "NullRabbit/v8-cipher-agnostic"
38
+ MULTICLASS_REPO = "NullRabbit/multiclass-folded"
39
+ DATASET_REPO = "NullRabbit/nr-bundles-public"
40
+
41
+
42
+ _models_cache: dict[str, Any] = {}
43
+
44
+
45
+ def _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
+
56
+ def _modality_state(bundle_dir: Path) -> tuple[bool, int, bool]:
57
+ responses_path = bundle_dir / "responses.parquet"
58
+ n_resp = 0
59
+ has_resp = False
60
+ if responses_path.is_file():
61
+ table = pq.read_table(responses_path)
62
+ n_resp = table.num_rows
63
+ has_resp = n_resp > 0
64
+ has_pcap = (bundle_dir / "packets.pcap").is_file()
65
+ return has_resp, n_resp, has_pcap
66
+
67
+
68
+ def _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 features
88
+
89
+
90
+ def _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's
94
+ OOD-by-construction behaviour on partial-coverage inputs is surfaced
95
+ 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] = value
115
+ return np.array([[features[n] for n in feature_names]], dtype=float)
116
+
117
+
118
+ def 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 = workdir
133
+ # 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 = upload
139
+ 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_pcap
179
+ else "resp_only" if has_resp
180
+ else "pcap_only" if has_pcap
181
+ else "none")
182
+
183
+ warning = None
184
+ 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.value
216
+ if hasattr(manifest.provenance.fidelity_class, "value")
217
+ else str(manifest.provenance.fidelity_class)
218
+ ),
219
+ "ground_truth_label": (
220
+ manifest.ground_truth_label.value
221
+ 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
+
241
+ DESCRIPTION = """
242
+ # nr-bundle-classifier
243
+
244
+ Run 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 family
247
+ - **[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
+
249
+ Upload 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
+
251
+ This 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
+
256
+ with 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
+
281
+ if __name__ == "__main__":
282
+ demo.launch()
release-cert.json ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audited_at": "2026-05-13T18:29:02Z",
3
+ "space_repo": "NullRabbit/nr-bundle-classifier",
4
+ "checks": [
5
+ {
6
+ "check": "app.py_exists",
7
+ "ok": true
8
+ },
9
+ {
10
+ "check": "README.md_exists",
11
+ "ok": true
12
+ },
13
+ {
14
+ "check": "requirements.txt_exists",
15
+ "ok": true
16
+ },
17
+ {
18
+ "check": "LICENSE_exists",
19
+ "ok": true
20
+ },
21
+ {
22
+ "check": "app_py_compiles",
23
+ "ok": true
24
+ },
25
+ {
26
+ "check": "readme_yaml_frontmatter",
27
+ "ok": true
28
+ },
29
+ {
30
+ "check": "readme_sdk_gradio",
31
+ "ok": true
32
+ },
33
+ {
34
+ "check": "readme_app_file_app_py",
35
+ "ok": true
36
+ },
37
+ {
38
+ "check": "readme_license_apache",
39
+ "ok": true
40
+ },
41
+ {
42
+ "check": "readme_anchor_autonomous_defence",
43
+ "ok": true
44
+ },
45
+ {
46
+ "check": "readme_anchor_iterative_leak_surface_peeling",
47
+ "ok": true
48
+ },
49
+ {
50
+ "check": "readme_methodology_is_the_contribution",
51
+ "ok": true
52
+ },
53
+ {
54
+ "check": "readme_substrate_paper_in_preparation",
55
+ "ok": true
56
+ },
57
+ {
58
+ "check": "readme_zenodo_doi_present",
59
+ "ok": true
60
+ },
61
+ {
62
+ "check": "readme_cross_links_nr_bundles_public",
63
+ "ok": true
64
+ },
65
+ {
66
+ "check": "readme_cross_links_v8",
67
+ "ok": true
68
+ },
69
+ {
70
+ "check": "readme_cross_links_multiclass",
71
+ "ok": true
72
+ },
73
+ {
74
+ "check": "readme_cross_links_nr_bundle_spec",
75
+ "ok": true
76
+ },
77
+ {
78
+ "check": "readme_has_honest_limitations",
79
+ "ok": true
80
+ },
81
+ {
82
+ "check": "readme_has_pcap_caveat",
83
+ "ok": true
84
+ },
85
+ {
86
+ "check": "readme_has_n1_bg01_caveat",
87
+ "ok": true
88
+ },
89
+ {
90
+ "check": "readme_no_embargo_language",
91
+ "ok": true
92
+ },
93
+ {
94
+ "check": "requirements_includes_gradio",
95
+ "ok": true
96
+ },
97
+ {
98
+ "check": "requirements_includes_huggingface_hub",
99
+ "ok": true
100
+ },
101
+ {
102
+ "check": "requirements_includes_scikit-learn",
103
+ "ok": true
104
+ },
105
+ {
106
+ "check": "requirements_includes_joblib",
107
+ "ok": true
108
+ },
109
+ {
110
+ "check": "requirements_includes_numpy",
111
+ "ok": true
112
+ },
113
+ {
114
+ "check": "requirements_includes_pyarrow",
115
+ "ok": true
116
+ },
117
+ {
118
+ "check": "requirements_includes_nr-bundle-spec",
119
+ "ok": true
120
+ },
121
+ {
122
+ "check": "app_imports_clean",
123
+ "ok": true
124
+ },
125
+ {
126
+ "check": "smoke_test_classify_bundle_returns_dict",
127
+ "ok": true,
128
+ "v8_verdict": "attack",
129
+ "v8_score": 0.9874,
130
+ "mc_verdict": "V14",
131
+ "mc_argmax_p": 0.4629
132
+ }
133
+ ],
134
+ "n_checks": 31,
135
+ "n_ok": 31,
136
+ "release_ok": true
137
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio>=4.0
2
+ huggingface_hub>=1.0
3
+ scikit-learn>=1.5
4
+ joblib>=1.3
5
+ numpy>=1.26
6
+ pyarrow>=17
7
+ git+https://github.com/NullRabbitLabs/nr-bundle-spec.git