anti-bla 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.codex-plugin/plugin.json +27 -0
- package/LICENSE +21 -0
- package/README.md +214 -2
- package/dist/src/cli.js +210 -0
- package/dist/src/commands/assemble.js +103 -0
- package/dist/src/commands/inspect.js +58 -0
- package/dist/src/commands/rea-evidence.js +97 -0
- package/dist/src/commands/report.js +34 -0
- package/dist/src/commands/setup.js +113 -0
- package/dist/src/commands/triage.js +54 -0
- package/dist/src/core/budgets.js +68 -0
- package/dist/src/core/classification.js +50 -0
- package/dist/src/core/files.js +178 -0
- package/dist/src/core/findings.js +64 -0
- package/dist/src/core/fingerprint.js +20 -0
- package/dist/src/core/json-dupes.js +63 -0
- package/dist/src/core/pipeline.js +95 -0
- package/dist/src/core/provenance.js +55 -0
- package/dist/src/core/types.js +2 -0
- package/dist/src/data/rea-failure-patterns.json +215 -0
- package/dist/src/data/rea-tools-6.8.0.json +45 -0
- package/dist/src/formats/asar.js +283 -0
- package/dist/src/formats/detect.js +89 -0
- package/dist/src/formats/generic.js +273 -0
- package/dist/src/formats/javascript.js +515 -0
- package/dist/src/formats/js-fold.js +147 -0
- package/dist/src/formats/js-measure.js +451 -0
- package/dist/src/formats/js-worker.js +13 -0
- package/dist/src/formats/sqlite.js +242 -0
- package/dist/src/formats/wasm.js +336 -0
- package/dist/src/rea/evidence-schema.js +26 -0
- package/dist/src/rea/failure-catalog.js +12 -0
- package/dist/src/rea/parse-evidence.js +194 -0
- package/dist/src/rea/recommend-next-step.js +189 -0
- package/dist/src/reports/json.js +29 -0
- package/dist/src/reports/markdown.js +122 -0
- package/docs/architecture.md +66 -0
- package/docs/benchmark-methodology.md +59 -0
- package/docs/compatibility.md +73 -0
- package/docs/development.md +51 -0
- package/docs/limitation-matrix.md +53 -0
- package/docs/releasing.md +49 -0
- package/docs/threat-model.md +80 -0
- package/docs/tool-contracts.md +151 -0
- package/package.json +65 -4
- package/skills/anti-bla/SKILL.md +111 -0
- package/skills/anti-bla/references/budget-recovery.md +28 -0
- package/skills/anti-bla/references/evidence-validation.md +53 -0
- package/skills/anti-bla/references/format-playbooks.md +66 -0
- package/skills/anti-bla/references/rea-integration.md +44 -0
- package/skills/anti-bla/references/reporting.md +50 -0
- package/skills/anti-bla/references/threat-model.md +57 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "anti-bla",
|
|
4
|
+
"description": "Anti-BLA skill for REA users.",
|
|
5
|
+
"owner": {
|
|
6
|
+
"name": "ShambaC",
|
|
7
|
+
"url": "https://github.com/ShambaC"
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "anti-bla",
|
|
12
|
+
"description": "Recognize and recover from BLA-style adversarial hardening alongside REA.",
|
|
13
|
+
"source": "./",
|
|
14
|
+
"category": "development"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anti-bla",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Overlay for the REA reverse-engineering skill: recognize and recover from BLA-style adversarial hardening with bounded, read-only, evidence-backed triage.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"reverse-engineering",
|
|
8
|
+
"rea",
|
|
9
|
+
"bla",
|
|
10
|
+
"agent-skill",
|
|
11
|
+
"static-analysis"
|
|
12
|
+
],
|
|
13
|
+
"skills": "./skills",
|
|
14
|
+
"repository": "https://github.com/ShambaC/anti-bla",
|
|
15
|
+
"homepage": "https://github.com/ShambaC/anti-bla#readme",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "ShambaC",
|
|
18
|
+
"url": "https://github.com/ShambaC"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "anti-bla",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Overlay for the REA reverse-engineering skill: recognize and recover from BLA-style adversarial hardening with bounded, read-only, evidence-backed triage.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"reverse-engineering",
|
|
8
|
+
"rea",
|
|
9
|
+
"bla",
|
|
10
|
+
"agent-skill"
|
|
11
|
+
],
|
|
12
|
+
"skills": "./skills",
|
|
13
|
+
"interface": {
|
|
14
|
+
"displayName": "Anti-BLA",
|
|
15
|
+
"shortDescription": "Analyze BLA-hardened artifacts with REA",
|
|
16
|
+
"category": "Developer Tools",
|
|
17
|
+
"capabilities": [
|
|
18
|
+
"Read"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"repository": "https://github.com/ShambaC/anti-bla",
|
|
22
|
+
"homepage": "https://github.com/ShambaC/anti-bla#readme",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "ShambaC",
|
|
25
|
+
"url": "https://github.com/ShambaC"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anti-BLA contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,215 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Anti-BLA
|
|
4
|
+
|
|
5
|
+
### See through anti-AI hardening. Keep REA working.
|
|
6
|
+
|
|
7
|
+
**A read-only overlay for [REA](https://github.com/morluto/rea). It recognizes artifacts hardened against AI reverse engineering (such as [BLA](https://github.com/qybaihe/bla)), measures each trap, and gets the analysis moving again with evidence for every step.**
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/anti-bla)
|
|
10
|
+
[](#quick-start)
|
|
11
|
+
[](docs/compatibility.md)
|
|
12
|
+
[](#results-against-real-bla)
|
|
13
|
+
[](skills/anti-bla/SKILL.md)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
|
|
16
|
+
[Quick start](#quick-start) · [Results against real BLA](#results-against-real-bla) · [How it works](#how-it-works) · [What it analyzes](#what-it-analyzes) · [FAQ](#faq) · [Documentation](#documentation)
|
|
17
|
+
|
|
18
|
+
<code>npx anti-bla setup</code>
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
Anti-BLA sits on top of REA. If you do not have REA yet, run `npx rea-agents setup` first ([REA quick start](https://github.com/morluto/rea#quick-start)).
|
|
25
|
+
|
|
26
|
+
### Set up your agent
|
|
27
|
+
|
|
28
|
+
With Node.js >= 22.13, run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx anti-bla setup
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Setup shows its plan before writing anything. It installs the `anti-bla` skill next to your other agent skills, in the same places REA's setup uses:
|
|
35
|
+
|
|
36
|
+
| Agent | Skill location |
|
|
37
|
+
| ---------------------------------------------------------------------------------- | --------------------------- |
|
|
38
|
+
| Claude Code | `~/.claude/skills/anti-bla` |
|
|
39
|
+
| Codex, Cursor, Gemini CLI and other [Agent Skills](https://agentskills.io) clients | `~/.agents/skills/anti-bla` |
|
|
40
|
+
|
|
41
|
+
Use `--project <dir>` to install into one project instead, `--dry-run` to preview, `--yes` to skip the prompt, and `--uninstall` to remove the skill. An existing modified copy is backed up first. Setup never edits REA, its skill, or any MCP configuration. Restart your agent afterward.
|
|
42
|
+
|
|
43
|
+
<details>
|
|
44
|
+
<summary><strong>Other ways to install</strong></summary>
|
|
45
|
+
|
|
46
|
+
<br />
|
|
47
|
+
|
|
48
|
+
| Route | Command |
|
|
49
|
+
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
50
|
+
| Skills CLI ([skills.sh](https://skills.sh)) | `npx skills add ShambaC/anti-bla` |
|
|
51
|
+
| Claude Code plugin | `/plugin marketplace add ShambaC/anti-bla`, then `/plugin install anti-bla@anti-bla` |
|
|
52
|
+
| Codex plugin | install from this repository (`.codex-plugin/plugin.json` points at `./skills`) |
|
|
53
|
+
| Manual | copy `skills/anti-bla/` into your agent's skills directory |
|
|
54
|
+
| Ask your agent | _"Install the Anti-BLA skill from https://github.com/ShambaC/anti-bla by running `npx anti-bla setup --yes`, then restart."_ |
|
|
55
|
+
| From source | `git clone https://github.com/ShambaC/anti-bla && cd anti-bla && npm ci && npm run build && node dist/src/cli.js setup` |
|
|
56
|
+
|
|
57
|
+
The CLI itself needs no installation: the skill calls `anti-bla` when it is on PATH (`npm install -g anti-bla`) and otherwise uses `npx -y anti-bla`.
|
|
58
|
+
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
### Ask your agent
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Understand how the license check works in the Notes app. If REA stalls or the code fights back,
|
|
65
|
+
use the anti-bla skill, recover what you can, and show me the evidence and what stays unknown.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Replace "the Notes app" with an app you own or are authorized to analyze, and "the license check" with the feature you want to understand. You do not need to name Anti-BLA every time. The skill loads on its own when REA hits a capacity limit or the target contains decoys or text aimed at the agent.
|
|
69
|
+
|
|
70
|
+
### Use the terminal
|
|
71
|
+
|
|
72
|
+
Triage an extracted app directory, an ASAR, or any single file:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx -y anti-bla triage /absolute/path/to/app --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The report lists each artifact's SHA-256, the traps it found with byte or AST locations, decoded values, suggested REA next steps, and what stays unknown. Replace the path with your target, such as `"D:/apps/example"` on Windows.
|
|
79
|
+
|
|
80
|
+
More commands:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx -y anti-bla inspect <file> [--member <path-in-asar>] # one file; one ASAR member analyzed in memory
|
|
84
|
+
npx -y anti-bla rea-evidence <saved-rea-output.json> # turn a REA error or Evidence record into supported next steps
|
|
85
|
+
npx -y anti-bla report <report.json> --format markdown # render a stored report
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
To install the `anti-bla` command for regular use, run `npm install --global anti-bla`, then `anti-bla --help`. Add `--json` for machine-readable output. All commands are read-only: targets are never executed, modified or extracted, and nothing is sent over the network.
|
|
89
|
+
|
|
90
|
+
## Results against real BLA
|
|
91
|
+
|
|
92
|
+
We wrote a small app with a known answer, hardened it with **real `bla-agents@3.2.1`** (`bla protect --intensity bankrupt`), and asked the same question three ways: REA on the clean original, REA alone on the hardened copy, and REA guided by Anti-BLA. REA is `rea-agents@6.8.0` over MCP. The hardened files were never executed.
|
|
93
|
+
|
|
94
|
+
| Target | BLA hardening | REA alone | REA + Anti-BLA | Recovered |
|
|
95
|
+
| :------------------------- | :----------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------: |
|
|
96
|
+
| **License gate** (`.js`) | 422 B → **136 KB** | ⚠️ partial: 4 semantic budgets hit (alias-selections, getter-expansion, primitive-bytes, primitive-candidates), **1,951 unknowns** | All 4 traps located. **31** opaque guards proven constant, **5** split strings decoded: `Acme Notes`, `string`, `pro`, `basic`, `free` | ✅ rules + tiers |
|
|
97
|
+
| **Electron app** (`.asar`) | 1.1 KB → **18.2 MB** index | ❌ refused: `truncated`, header admission 16 MiB, no required size given | Measured header **17,443,968 B**, set `asar.max_header_bytes`, retried **once**: analysis admitted. Decoded IPC channel `notes:quota` and bridge `notes` that REA left dynamic | ✅ channel + bridge |
|
|
98
|
+
| **WebAssembly** (`.wasm`) | 41 B → **6.9 MB**, 251 exports | ❌ provider unavailable here (WABT 1.0.42 not configured)¹ | **251 exports → 1 real function** (9-byte body, exact byte range). 9 decoy custom sections, canary credentials flagged and redacted | 🟡 body located, read by hand |
|
|
99
|
+
| **SQLite** (`.sqlite`) | 8 KB → 53 KB, 5 honey tables | ❌ provider unavailable here (needs Node ≥ 24.15)¹ | Real table `licenses` identified. 3 trap columns on `enclave_unseal_payload()` isolated to decoy tables; nothing evaluated | ✅ schema level |
|
|
100
|
+
|
|
101
|
+
Clean controls: Anti-BLA ran on the four unhardened originals and reported **0** medium/high findings and attribution `none-observed`. Anti-BLA's own wall time is **< 1 s** for all four hardened targets.
|
|
102
|
+
|
|
103
|
+
<sub>¹ BLA's own report shows REA hitting `WASM_INTERFACE_REPRESENTATION_BYTES` and `SqliteColumnEvaluationFailure` when those providers are installed. We did not reproduce that here, and we do not claim it.</sub>
|
|
104
|
+
|
|
105
|
+
<details>
|
|
106
|
+
<summary><strong>What "recovered" means, and what it does not</strong></summary>
|
|
107
|
+
|
|
108
|
+
<br />
|
|
109
|
+
|
|
110
|
+
- **Decoded strings** come from sound static folding: literals, bitwise operations, `Math.imul` and `?:`, evaluated across every possible value of BLA's runtime seed. BLA ties one seed bit to a runtime digest check, so each string folds to at most two candidates. The report lists both (e.g. `Acme Notes` or `A8me2No9es`).
|
|
111
|
+
- **Opaque guards** such as `(Math.imul(x|85,(x|85)+1)&1)===0` are proven constant for every possible seed value, not guessed.
|
|
112
|
+
- **REA's semantic budgets stay hit.** Anti-BLA does not raise or bypass them; there is no supported setting for them. Anti-BLA answers the question from its own evidence and keeps REA's unknowns in the report.
|
|
113
|
+
- One run, single operator, graded against known ground truth. Machine-readable record: [`benchmarks/results/bla-showcase-2026-11-10.json`](benchmarks/results/bla-showcase-2026-11-10.json).
|
|
114
|
+
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
Reproduce it:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/ShambaC/anti-bla && cd anti-bla
|
|
121
|
+
npm ci
|
|
122
|
+
npm install --no-save bla-agents@3.2.1 # BLA is only needed to build the hardened samples
|
|
123
|
+
npm run bench:bla # hardens the owned samples in a temp dir and runs Anti-BLA on both sets
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The run writes `benchmarks/results/bla-showcase-latest.json`. The REA columns come from REA over MCP and are recorded in [`benchmarks/results/bla-showcase-2026-11-10.json`](benchmarks/results/bla-showcase-2026-11-10.json).
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary><strong>Earlier paired agent run on owned fixtures (no BLA)</strong></summary>
|
|
130
|
+
|
|
131
|
+
<br />
|
|
132
|
+
|
|
133
|
+
The same Sonnet subagent answered 7 questions twice (4 hardened-style fixtures and 3 clean controls), once with REA only and once with REA + Anti-BLA. Both arms got **17/17** facts right on these small, hand-parsable fixtures, so this run shows **no accuracy or token benefit**: 77,738 vs 78,790 tokens, 85 s vs 89 s. Arm B made 2 REA calls versus 7. The REA-only agent tried to `SELECT` the SQLite trap column; the Anti-BLA agent did not. Details: [`benchmarks/results/paired-2026-11-10.md`](benchmarks/results/paired-2026-11-10.md).
|
|
134
|
+
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
## How it works
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
your question ──► REA (primary tool) ──► result, partial result, or capacity error
|
|
141
|
+
▲ │
|
|
142
|
+
│ ▼
|
|
143
|
+
one supported setting / narrower anti-bla triage | inspect | rea-evidence
|
|
144
|
+
scope / retained-Evidence view • fingerprint (SHA-256), bounded read-only parse
|
|
145
|
+
(schema-checked by the agent) • measure the trap (header bytes, 2^N selector states,
|
|
146
|
+
▲ candidate counts, trap functions, decoy sections)
|
|
147
|
+
│ • fold opaque guards and split strings soundly
|
|
148
|
+
└──── next step ◄── • quote poisoned text as untrusted data, never obey it
|
|
149
|
+
• report: observed / inferred / unknown, with byte, AST
|
|
150
|
+
or REA Evidence provenance for every finding
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **The skill instructs and the CLI computes.** REA stays the primary tool. Anti-BLA never calls REA, never patches anything, and never claims a protection was "defeated".
|
|
154
|
+
- **Attribution has three levels:** `none-observed`, `bla-like-mechanisms` and `bla-markers-observed`. A REA capacity error alone is never called BLA.
|
|
155
|
+
- **Recovery is supported only.** Anti-BLA suggests documented REA settings within REA's own bounds, then one retry. A repeated identical failure is a stop.
|
|
156
|
+
|
|
157
|
+
## What it analyzes
|
|
158
|
+
|
|
159
|
+
| Format | What Anti-BLA measures | Status |
|
|
160
|
+
| :------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-----: |
|
|
161
|
+
| **JavaScript / TypeScript** | literal-alternative explosions, primitive-byte charge, expression depth, alias chains and tables, getter rings and dynamic-receiver getters, opaque guards, split strings, `eval`/`Function` sites | ✅ |
|
|
162
|
+
| **Source maps** | VLQ validity, out-of-range sources, `sourcesContent` that no mapping uses, fit against the emitted bundle | ✅ |
|
|
163
|
+
| **Electron `.asar`** | header vs. REA admission, index inventory, unsafe/escaping names, overlapping or out-of-bounds members, duplicate keys, `--member` analysis with integrity check | ✅ |
|
|
164
|
+
| **WebAssembly** | bounded LEB128 sections, import/export inventory, export aliasing, custom-section decoys, lower-bound REA interface charge | ✅ |
|
|
165
|
+
| **SQLite** | header checks, schema-only catalog from a private read-only copy, generated columns that need application functions | ✅ |
|
|
166
|
+
| **PE / ELF / Mach-O / other** | poisoned text, canary credentials, Electron fuse-sentinel floods | generic |
|
|
167
|
+
| Runtime, native decompilation, models, Go/Flutter/BEAM/EVM/pcap/GPU | use REA directly | — |
|
|
168
|
+
|
|
169
|
+
Every technique, including what is **not** detected, is listed in the [limitation matrix](docs/limitation-matrix.md).
|
|
170
|
+
|
|
171
|
+
## FAQ
|
|
172
|
+
|
|
173
|
+
<details>
|
|
174
|
+
<summary><strong>Does Anti-BLA replace REA?</strong></summary>
|
|
175
|
+
|
|
176
|
+
No. REA does the reverse engineering. Anti-BLA spots when an artifact is fighting the analysis, measures how, and tells the agent which supported REA step to try next. Without REA, the CLI still triages files, and the skill tells the agent to say that REA-based conclusions are unavailable.
|
|
177
|
+
|
|
178
|
+
</details>
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary><strong>Is it safe to run on hostile files?</strong></summary>
|
|
182
|
+
|
|
183
|
+
It is built for hostile input. Anti-BLA does not execute, `require` or instantiate targets, and does not evaluate SQLite columns. Reads are bounded and every regex quantifier is bounded. JS parsing runs in a resource-limited worker. Prompt-injection text is quoted as data and never obeyed, and apparent secrets are redacted. See the [threat model](docs/threat-model.md).
|
|
184
|
+
|
|
185
|
+
</details>
|
|
186
|
+
|
|
187
|
+
<details>
|
|
188
|
+
<summary><strong>Can it be fooled?</strong></summary>
|
|
189
|
+
|
|
190
|
+
Yes. Detection is structural and versioned against REA 6.8.0 and BLA 3.2.1. New hardening shapes can slip past it, markers can be planted, and large legitimate apps can trip capacity rules. Findings separate what was observed from what is inferred, and attribution never reads "confirmed".
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
194
|
+
<details>
|
|
195
|
+
<summary><strong>Is this legal to use?</strong></summary>
|
|
196
|
+
|
|
197
|
+
Use it only on software you own, have permission to analyze, or that was published as a challenge. Anti-BLA does not bypass licensing, integrity enforcement or anti-debugging, and it performs no runtime instrumentation.
|
|
198
|
+
|
|
199
|
+
</details>
|
|
200
|
+
|
|
201
|
+
## Documentation
|
|
202
|
+
|
|
203
|
+
[Architecture](docs/architecture.md) · [Threat model](docs/threat-model.md) · [Tool contracts](docs/tool-contracts.md) · [Compatibility (pinned REA/BLA)](docs/compatibility.md) · [Limitation matrix](docs/limitation-matrix.md) · [Benchmark methodology](docs/benchmark-methodology.md) · [Development](docs/development.md) · [Releasing](docs/releasing.md) · [Skill](skills/anti-bla/SKILL.md)
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm ci && npm run build
|
|
209
|
+
npm test # unit, integration, regression and security tests
|
|
210
|
+
npm run check:skill # frontmatter, links, and REA tool names vs. the pinned REA 6.8.0 catalog
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT. REA and BLA are separate MIT-licensed projects. Anti-BLA includes none of their code or output, and the test corpus is generated independently.
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// anti-bla: read-only, bounded triage CLI. Exit codes: 0 ok, 1 analysis failed, 2 usage/input error.
|
|
3
|
+
import { Budget, CLI_LIMIT_FLAGS, DEFAULT_LIMITS } from './core/budgets.js';
|
|
4
|
+
import { TOOL_VERSION, UsageError, writeOutput } from './commands/assemble.js';
|
|
5
|
+
import { runTriage } from './commands/triage.js';
|
|
6
|
+
import { runInspect, INSPECT_FORMATS } from './commands/inspect.js';
|
|
7
|
+
import { runReaEvidence } from './commands/rea-evidence.js';
|
|
8
|
+
import { runReport } from './commands/report.js';
|
|
9
|
+
import { CLIENTS, runSetup } from './commands/setup.js';
|
|
10
|
+
import { toJson } from './reports/json.js';
|
|
11
|
+
const LIMIT_HELP = Object.entries(CLI_LIMIT_FLAGS).map(([f, k]) => ` ${f.padEnd(18)} default ${DEFAULT_LIMITS[k]}`).join('\n');
|
|
12
|
+
const COMMON = `Common options:
|
|
13
|
+
--json machine-readable report on stdout (diagnostics on stderr; with --output the report goes only to the file)
|
|
14
|
+
--output <file> write the report atomically to <file> (never inside the analyzed input)
|
|
15
|
+
--force allow --output to replace an existing file
|
|
16
|
+
--redact-paths replace absolute artifact paths with <redacted>/<logical path>
|
|
17
|
+
Budget caps (hard limits):
|
|
18
|
+
${LIMIT_HELP}`;
|
|
19
|
+
const HELP = {
|
|
20
|
+
main: `anti-bla ${TOOL_VERSION} — read-only triage of adversarially hardened artifacts; companion to REA.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
anti-bla triage <path> [options] walk a file or directory, analyze every file
|
|
24
|
+
anti-bla inspect <file> [--format f] [--member p] [options]
|
|
25
|
+
anti-bla rea-evidence <evidence.json>... [options]
|
|
26
|
+
anti-bla report <report.json> [--format markdown|json] [--output f]
|
|
27
|
+
anti-bla setup [--client claude,agents] [--project <dir>] [--dry-run] [--yes] [--uninstall]
|
|
28
|
+
anti-bla --version | --help | <command> --help
|
|
29
|
+
|
|
30
|
+
Anti-BLA never executes, modifies or extracts targets, never contacts the network,
|
|
31
|
+
and never calls REA itself: it recommends REA actions the host agent must validate.
|
|
32
|
+
|
|
33
|
+
${COMMON}`,
|
|
34
|
+
triage: `anti-bla triage <path> [options]
|
|
35
|
+
Walks <path> in stable order (symlinks outside the root are not followed), hashes each
|
|
36
|
+
file, detects format by magic bytes/structure, runs bounded analyzers, emits one report.
|
|
37
|
+
|
|
38
|
+
${COMMON}`,
|
|
39
|
+
inspect: `anti-bla inspect <file> [--format ${INSPECT_FORMATS.join('|')}] [--member <asar member>] [options]
|
|
40
|
+
One format-specific inspection. --member selects one ASAR member, analyzed in memory only.
|
|
41
|
+
|
|
42
|
+
${COMMON}`,
|
|
43
|
+
'rea-evidence': `anti-bla rea-evidence <file.json>... [options]
|
|
44
|
+
Parses REA Evidence records, MCP tool results, CLI error envelopes or exported bundles.
|
|
45
|
+
Preserves Evidence IDs, limitations, unknowns and coverage pointers; emits conditional
|
|
46
|
+
REA next steps. Paths inside the JSON are never opened.
|
|
47
|
+
|
|
48
|
+
${COMMON}`,
|
|
49
|
+
setup: `anti-bla setup [--client claude,agents] [--project <dir>] [--dry-run] [--yes] [--uninstall]
|
|
50
|
+
Installs the anti-bla skill next to your other agent skills (the same places REA's setup uses):
|
|
51
|
+
claude -> ~/.claude/skills/anti-bla (or <project>/.claude/skills with --project)
|
|
52
|
+
agents -> ~/.agents/skills/anti-bla (Codex, Cursor, Gemini CLI and other Agent Skills clients)
|
|
53
|
+
Default: both when ~/.claude exists, otherwise agents. Shows the plan first; writes only with --yes
|
|
54
|
+
or an interactive y. Existing different copies are backed up. Never edits REA or MCP configuration.`,
|
|
55
|
+
report: `anti-bla report <report.json> [--format markdown|json] [--output <file>] [--force]
|
|
56
|
+
Renders a stored anti-bla-report/v1 without reopening any target.`,
|
|
57
|
+
};
|
|
58
|
+
const VALUE_FLAGS = new Set(['--output', '--format', '--member', '--client', '--project', ...Object.keys(CLI_LIMIT_FLAGS)]);
|
|
59
|
+
const BOOL_FLAGS = new Set(['--json', '--force', '--help', '-h', '--version', '-V', '--redact-paths', '--dry-run', '--yes', '-y', '--uninstall']);
|
|
60
|
+
function parse(argv) {
|
|
61
|
+
const flags = new Map();
|
|
62
|
+
const positionals = [];
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
const a = argv[i];
|
|
65
|
+
if (a === '--') {
|
|
66
|
+
positionals.push(...argv.slice(i + 1));
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (a.startsWith('-')) {
|
|
70
|
+
const [name, inline] = a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, undefined];
|
|
71
|
+
if (VALUE_FLAGS.has(name)) {
|
|
72
|
+
const v = inline ?? argv[++i];
|
|
73
|
+
if (v === undefined)
|
|
74
|
+
throw new UsageError(`${name} needs a value`);
|
|
75
|
+
flags.set(name, v);
|
|
76
|
+
}
|
|
77
|
+
else if (BOOL_FLAGS.has(name))
|
|
78
|
+
flags.set(name, true);
|
|
79
|
+
else
|
|
80
|
+
throw new UsageError(`Unknown option ${name}`);
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
positionals.push(a);
|
|
84
|
+
}
|
|
85
|
+
return { command: positionals.shift() ?? '', positionals, flags };
|
|
86
|
+
}
|
|
87
|
+
function budgetFrom(flags) {
|
|
88
|
+
const lim = {};
|
|
89
|
+
for (const [flag, key] of Object.entries(CLI_LIMIT_FLAGS)) {
|
|
90
|
+
const v = flags.get(flag);
|
|
91
|
+
if (v === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
const n = Number(v);
|
|
94
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
95
|
+
throw new UsageError(`${flag} must be a positive integer`);
|
|
96
|
+
lim[key] = n;
|
|
97
|
+
}
|
|
98
|
+
return new Budget(lim);
|
|
99
|
+
}
|
|
100
|
+
function summary(r) {
|
|
101
|
+
const L = [`anti-bla ${r.command}: ${r.analysisStatus.toUpperCase()} · ${r.artifacts.length} artifact(s) · ${r.findings.length} finding(s) · attribution ${r.attribution.status}`];
|
|
102
|
+
for (const f of r.findings.slice(0, 20))
|
|
103
|
+
L.push(` [${f.confidence}] ${f.classification} ${f.id}: ${f.title}`);
|
|
104
|
+
if (r.findings.length > 20)
|
|
105
|
+
L.push(` … ${r.findings.length - 20} more (use --json)`);
|
|
106
|
+
for (const a of r.suggestedActions.slice(0, 10))
|
|
107
|
+
L.push(` next (${a.kind}, ${a.risk}): ${a.title}`);
|
|
108
|
+
if (r.coverage.excluded.length)
|
|
109
|
+
L.push(` excluded: ${r.coverage.excluded.length} path(s)`);
|
|
110
|
+
if (r.budgets.truncated)
|
|
111
|
+
L.push(' budgets truncated: results are partial');
|
|
112
|
+
if (r.unknowns.length)
|
|
113
|
+
L.push(` unknowns: ${r.unknowns.length} (see --json / report)`);
|
|
114
|
+
return L.join('\n') + '\n';
|
|
115
|
+
}
|
|
116
|
+
export async function main(argv) {
|
|
117
|
+
let json = argv.includes('--json');
|
|
118
|
+
try {
|
|
119
|
+
const p = parse(argv);
|
|
120
|
+
json = p.flags.has('--json');
|
|
121
|
+
if (p.flags.has('--version') || p.flags.has('-V')) {
|
|
122
|
+
process.stdout.write(`${TOOL_VERSION}\n`);
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
if (!p.command || (p.flags.has('--help') || p.flags.has('-h')) && !HELP[p.command]) {
|
|
126
|
+
process.stdout.write(HELP.main + '\n');
|
|
127
|
+
return p.command ? 2 : 0;
|
|
128
|
+
}
|
|
129
|
+
if (p.flags.has('--help') || p.flags.has('-h')) {
|
|
130
|
+
process.stdout.write(HELP[p.command] + '\n');
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
const out = p.flags.get('--output');
|
|
134
|
+
const force = p.flags.has('--force');
|
|
135
|
+
const redactPaths = p.flags.has('--redact-paths');
|
|
136
|
+
if (p.command === 'setup') {
|
|
137
|
+
const o = { dryRun: p.flags.has('--dry-run'), yes: p.flags.has('--yes') || p.flags.has('-y'), uninstall: p.flags.has('--uninstall') };
|
|
138
|
+
const c = p.flags.get('--client');
|
|
139
|
+
if (typeof c === 'string') {
|
|
140
|
+
const list = c.split(',').map((x) => x.trim()).filter(Boolean);
|
|
141
|
+
for (const x of list)
|
|
142
|
+
if (!CLIENTS.includes(x))
|
|
143
|
+
throw new UsageError(`Unknown --client ${x}; expected ${CLIENTS.join(', ')}`);
|
|
144
|
+
o.clients = list;
|
|
145
|
+
}
|
|
146
|
+
const proj = p.flags.get('--project');
|
|
147
|
+
if (typeof proj === 'string')
|
|
148
|
+
o.project = proj;
|
|
149
|
+
await runSetup(o);
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
if (p.command === 'report') {
|
|
153
|
+
if (p.positionals.length !== 1)
|
|
154
|
+
throw new UsageError('report needs exactly one report.json');
|
|
155
|
+
const fmt = String(p.flags.get('--format') ?? 'markdown');
|
|
156
|
+
const text = await runReport(p.positionals[0], fmt);
|
|
157
|
+
if (typeof out === 'string')
|
|
158
|
+
await writeOutput(out, text, force, [p.positionals[0]]);
|
|
159
|
+
else
|
|
160
|
+
process.stdout.write(text);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
let report;
|
|
164
|
+
const budget = budgetFrom(p.flags);
|
|
165
|
+
if (p.command === 'triage') {
|
|
166
|
+
if (p.positionals.length !== 1)
|
|
167
|
+
throw new UsageError('triage needs exactly one path');
|
|
168
|
+
report = await runTriage(p.positionals[0], budget, { redactPaths });
|
|
169
|
+
}
|
|
170
|
+
else if (p.command === 'inspect') {
|
|
171
|
+
if (p.positionals.length !== 1)
|
|
172
|
+
throw new UsageError('inspect needs exactly one file');
|
|
173
|
+
const o = { redactPaths };
|
|
174
|
+
if (typeof p.flags.get('--format') === 'string')
|
|
175
|
+
o.format = p.flags.get('--format');
|
|
176
|
+
if (typeof p.flags.get('--member') === 'string')
|
|
177
|
+
o.member = p.flags.get('--member');
|
|
178
|
+
report = await runInspect(p.positionals[0], budget, o);
|
|
179
|
+
}
|
|
180
|
+
else if (p.command === 'rea-evidence') {
|
|
181
|
+
report = await runReaEvidence(p.positionals, budget, { redactPaths });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
throw new UsageError(`Unknown command ${p.command}; see anti-bla --help`);
|
|
185
|
+
}
|
|
186
|
+
const body = toJson(report);
|
|
187
|
+
if (typeof out === 'string') {
|
|
188
|
+
await writeOutput(out, body, force, p.positionals);
|
|
189
|
+
process.stderr.write(`report written: ${out}\n`);
|
|
190
|
+
}
|
|
191
|
+
if (json && typeof out !== 'string')
|
|
192
|
+
process.stdout.write(body);
|
|
193
|
+
else if (!json)
|
|
194
|
+
process.stdout.write(summary(report));
|
|
195
|
+
return report.analysisStatus === 'failed' ? 1 : 0;
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
const err = e;
|
|
199
|
+
const code = e instanceof UsageError ? err.code : 'internal_error';
|
|
200
|
+
if (json)
|
|
201
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 'anti-bla-error/v1', error: { code, message: err.message } }, null, 2) + '\n');
|
|
202
|
+
else
|
|
203
|
+
process.stderr.write(`anti-bla: ${err.message}\n`);
|
|
204
|
+
return 2;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Run only as the entry point (tests import main()).
|
|
208
|
+
const entry = process.argv[1] ? (await import('node:url')).pathToFileURL((await import('node:fs')).realpathSync(process.argv[1])).href : '';
|
|
209
|
+
if (import.meta.url === entry)
|
|
210
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { aggregateStatus, attribution } from '../core/classification.js';
|
|
5
|
+
import { capFindings } from '../core/findings.js';
|
|
6
|
+
import { isInside } from '../core/files.js';
|
|
7
|
+
import { REPORT_SCHEMA_VERSION } from '../core/types.js';
|
|
8
|
+
// Single source of truth: the package version (dist/src/commands -> package root).
|
|
9
|
+
export const TOOL_VERSION = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8')).version;
|
|
10
|
+
export class UsageError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
constructor(message, code = 'invalid_input') {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const emptyCollected = () => ({ artifacts: [], statuses: [], findings: [], actions: [], examined: [], excluded: [], limitations: [], unknowns: [] });
|
|
18
|
+
const evKey = (f) => {
|
|
19
|
+
const e = f.evidence[0];
|
|
20
|
+
return [e.logicalPath ?? '', String(e.byteStart ?? -1).padStart(16, '0'), f.rule, f.id].join('\u0000');
|
|
21
|
+
};
|
|
22
|
+
export function assemble(command, c, budget, opts = {}) {
|
|
23
|
+
const { kept, dropped } = capFindings(c.findings, budget.limits.maxFindings);
|
|
24
|
+
const limitations = [...c.limitations];
|
|
25
|
+
if (dropped) {
|
|
26
|
+
limitations.push(`${dropped} finding(s) dropped by --max-findings ${budget.limits.maxFindings} (lowest confidence first).`);
|
|
27
|
+
budget.markTruncated('max findings reached');
|
|
28
|
+
}
|
|
29
|
+
for (const r of budget.reasons)
|
|
30
|
+
limitations.push(`Budget: ${r}.`);
|
|
31
|
+
const findings = [...kept].sort((a, b) => (evKey(a) < evKey(b) ? -1 : 1));
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const actions = c.actions.filter((a) => { const k = JSON.stringify([a.kind, a.title, a.operation]); if (seen.has(k))
|
|
34
|
+
return false; seen.add(k); return true; });
|
|
35
|
+
const uniq = (xs) => [...new Set(xs)];
|
|
36
|
+
const report = {
|
|
37
|
+
schemaVersion: REPORT_SCHEMA_VERSION,
|
|
38
|
+
command,
|
|
39
|
+
analysisStatus: aggregateStatus(c.statuses, c.excluded.length > 0 || budget.truncated),
|
|
40
|
+
analyzedAt: new Date().toISOString(),
|
|
41
|
+
toolVersion: TOOL_VERSION,
|
|
42
|
+
artifacts: c.artifacts.map((a) => (opts.redactPaths ? { ...a, path: `<redacted>/${a.logicalPath}` } : a)),
|
|
43
|
+
findings,
|
|
44
|
+
suggestedActions: actions,
|
|
45
|
+
coverage: { examined: c.examined, excluded: c.excluded },
|
|
46
|
+
limitations: uniq(limitations),
|
|
47
|
+
unknowns: uniq(c.unknowns),
|
|
48
|
+
attribution: attribution(findings),
|
|
49
|
+
budgets: { elapsedMs: budget.elapsedMs(), bytesRead: budget.bytesRead, bytesHashed: budget.bytesHashed, truncated: budget.truncated, limits: { ...budget.limits } },
|
|
50
|
+
};
|
|
51
|
+
if (opts.rea)
|
|
52
|
+
report.rea = opts.rea;
|
|
53
|
+
return report;
|
|
54
|
+
}
|
|
55
|
+
/** Atomic write to an explicit destination; refuses overwrite without --force and refuses paths inside analyzed inputs. */
|
|
56
|
+
export async function writeOutput(outPath, content, force, protectedPaths) {
|
|
57
|
+
const out = resolve(outPath);
|
|
58
|
+
// Compare real paths (symlinks/junctions/8.3 names resolved) and, on Windows/macOS, case-insensitively.
|
|
59
|
+
let realOut = out;
|
|
60
|
+
try {
|
|
61
|
+
realOut = join(await realpath(dirname(out)), basename(out));
|
|
62
|
+
}
|
|
63
|
+
catch { /* parent missing: writeFile will fail */ }
|
|
64
|
+
const fold = (p) => (process.platform === 'win32' || process.platform === 'darwin' ? p.toLowerCase() : p);
|
|
65
|
+
let outStat = null;
|
|
66
|
+
try {
|
|
67
|
+
outStat = await stat(out, { bigint: true });
|
|
68
|
+
}
|
|
69
|
+
catch { /* absent */ }
|
|
70
|
+
for (const p of protectedPaths) {
|
|
71
|
+
let abs = resolve(p);
|
|
72
|
+
try {
|
|
73
|
+
abs = await realpath(abs);
|
|
74
|
+
}
|
|
75
|
+
catch { /* gone */ }
|
|
76
|
+
let st = null;
|
|
77
|
+
try {
|
|
78
|
+
st = await stat(abs, { bigint: true });
|
|
79
|
+
}
|
|
80
|
+
catch { /* gone */ }
|
|
81
|
+
const sameFile = !!(st && outStat && st.ino !== 0n && st.ino === outStat.ino && st.dev === outStat.dev);
|
|
82
|
+
if (sameFile || fold(realOut) === fold(abs) || (st?.isDirectory() && isInside(fold(abs), fold(realOut)))) {
|
|
83
|
+
throw new UsageError(`Refusing to write output inside or onto an analyzed input: ${out}`, 'unsafe_output');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let exists = false;
|
|
87
|
+
try {
|
|
88
|
+
await stat(out);
|
|
89
|
+
exists = true;
|
|
90
|
+
}
|
|
91
|
+
catch { /* absent */ }
|
|
92
|
+
if (exists && !force)
|
|
93
|
+
throw new UsageError(`Output exists: ${out} (use --force to overwrite)`, 'output_exists');
|
|
94
|
+
const tmp = `${out}.anti-bla-${process.pid}.tmp`;
|
|
95
|
+
try {
|
|
96
|
+
await writeFile(tmp, content, { flag: 'wx' });
|
|
97
|
+
await rename(tmp, out);
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
await rm(tmp, { force: true });
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
}
|