aletheia-firewall 0.3.0 → 0.4.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/.helios-baseline CHANGED
@@ -1 +1 @@
1
- 800c13e8709801a5cd30d832a05d9e1fe0fd51780af348860c35a430d92957c4
1
+ 7f88b7011cdf41ed82ce047932b27fee86e36aca3cc010845acdcea0253c5e5b
package/README.md CHANGED
@@ -20,6 +20,28 @@ For Bun:
20
20
  FW_ENABLE_DETECTION=1 BUN_PRELOAD=aletheia-firewall bun app.js
21
21
  ```
22
22
 
23
+ ## Coverage & Limitations
24
+
25
+ Aletheia hooks `Module.prototype._compile`, Node's CommonJS compilation step. This means:
26
+
27
+ | Load path | Covered |
28
+ |---|---|
29
+ | `require()` of `.js` / `.cjs` | ✅ |
30
+ | `import` / `import()` (ESM, `.mjs`) | ❌ separate Node loader, not hooked |
31
+ | `.json` requires | ❌ handled by Node core, bypasses `_compile` |
32
+ | Native addons (`.node`) | ❌ not JS, not scanned |
33
+ | Dependency npm lifecycle scripts (preinstall/postinstall) | ❌ run before the firewall loads |
34
+
35
+ Aletheia is a **runtime enforcement layer**: it watches what a dependency does once it's
36
+ already in your CommonJS require graph, after `npm install` has finished. It is not an
37
+ install-time scanner and does not intercept package installation.
38
+
39
+ Detection is signature + behavioral, not AST-based, so payloads can evade static matching
40
+ through string-splitting, encoding, or indirection. Our adversarial corpus documents this
41
+ honestly: **95/125 (76%) of malicious payloads caught, 30 known bypasses, 0 false positives**
42
+ on the current corpus — run it yourself from the repository root with `npm run redteam`. Every bypass class is listed
43
+ in [`red-team/README.md`](../../red-team/README.md).
44
+
23
45
  ## Environment Variables
24
46
 
25
47
  | Variable | Default | Description |
@@ -66,18 +88,18 @@ own key (`scripts/generate-policy-key.js`) and set `FW_POLICY_PUBKEY`.
66
88
 
67
89
  ## Performance
68
90
 
69
- The firewall's cost is a **one-time per-module compile scan** — the `Module._compile` hook runs once per file on first load, then a compilation cache short-circuits it. There is **zero overhead when `FW_ENABLE_DETECTION` is unset** (`index.js` returns immediately and installs no hook).
91
+ The firewall's cost is a **one-time per-module compile scan** — the `Module._compile` hook runs once per file on first load, then a compilation cache short-circuits repeat compilations. There is **zero overhead when `FW_ENABLE_DETECTION` is unset** (`index.js` returns immediately and installs no hook).
70
92
 
71
- Measured on a 900-module cold load (methodology: `packages/fw-control/test/bench.js` in the monorepo), AMD EPYC, Node v22 (CI: 18, 20, 22), Linux x64:
93
+ The repo maintains a 25% median compilation-overhead gate budget, but this is a regression threshold, not a published release guarantee. Current v0.3.0 evidence shows the runtime `Module._compile` interception path is the dominant steady-state cost; `Detector.scanModuleSync` is a secondary contributor in the verified 900-module workload.
72
94
 
73
- | Metric | Measured | Gate budget | Enforced? |
74
- |--------|----------|-------------|-----------|
75
- | Median module-compile overhead | ~17–21% (varies by host) | 25% | **Yes** |
76
- | P95 overhead | ~25–37% across hosts | 30% (reference) | No — informational |
95
+ | Metric | Budget | Enforced? |
96
+ |--------|--------|-----------|
97
+ | Median module-compile overhead | 25% | **Yes** |
98
+ | P95 overhead | 30% (informational only) | No |
77
99
 
78
- Environment: measured on two AMD EPYC Codespaces 9V74 (80-core) and 7763 (64-core), Node v22. The median is host-dependent (7763 ~17%, 9V74 ~20–21%); after the v0.1.0 sub-512B scan-skip fix the measured range is ~17–21%.
100
+ The gate is a **regression guard**, not a performance target. If the median exceeds 25%, the change needs review.
79
101
 
80
- The gate **enforces median only** (budget 25%). P95 (~25–37%) is informational and **not stable across hardware** it reflects shared-CPU scheduler contention on multi-tenant Codespaces, not firewall algorithmic cost — so it is reported but never gated.
102
+ For the v0.3.0 frozen baseline and diagnostic evidence, see `PERFORMANCE.md` and `results/benchmarks/steady-state-compile-attr-*.json`.
81
103
 
82
104
  To reproduce, run the 900-module gate from the GitHub repo: `npm run gate`.
83
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aletheia-firewall",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Zero-dependency runtime firewall that blocks malicious npm modules at require-time.",
5
5
  "main": "index.js",
6
6
  "license": "MIT",
@@ -2,6 +2,8 @@
2
2
  // Behavioral analyzer for sequence-based threat detection.
3
3
  // Tracks dangerous action sequences within a single module.
4
4
 
5
+ const { AhoCorasick } = require('./aho-corasick');
6
+
5
7
  // Signal detection patterns for each behavioral category
6
8
  const SIGNAL_PATTERNS = {
7
9
  // Reads sensitive credential files (fs-based). process.env is tracked separately
@@ -155,6 +157,47 @@ function matchesAny(content, patterns) {
155
157
  return patterns.some(p => p.test(content));
156
158
  }
157
159
 
160
+ // Literal substrings that are a superset of all SIGNAL_PATTERN regexes.
161
+ // Used as a fast pre-screener: if none appear in content, all signal checks are
162
+ // guaranteed false and the expensive scanSrc normalization + regex loop can be
163
+ // skipped. A false positive (keyword present but regex doesn't match) is safe;
164
+ // a false negative would miss a detection and is prevented by the superset property.
165
+ const BEHAVIOR_PRESCREENER_KEYWORDS = [
166
+ // SENSITIVE_READ
167
+ 'readfile', 'fs.open',
168
+ // ENV_READ
169
+ 'process.env',
170
+ // SENSITIVE_PATH
171
+ '.env', 'credentials', '.ssh', 'id_rsa', '.netrc', '.aws', 'secret', 'passwd', 'shadow',
172
+ // SENSITIVE_CONFIG_PATH
173
+ '.kube', '.docker', 'login data',
174
+ // NPMRC_READ / NPMRC_TOKEN
175
+ '.npmrc', '_authtoken', '_auth', '_password', 'authtoken',
176
+ // HOST_OPTION
177
+ 'host:',
178
+ // NETWORK_EGRESS
179
+ 'http.request', 'https.request', 'http.get', 'https.get',
180
+ 'fetch(', 'net.connect', 'net.createconnection', 'socket.connect',
181
+ 'websocket', 'xmlhttprequest', 'tls.connect', 'dgram.createsocket',
182
+ 'dns.resolve', 'sendbeacon',
183
+ // inline require("http"|"https"|"net"|"tls"|"dgram").method() forms
184
+ 'require("http', "require('http", 'require("https', "require('https",
185
+ 'require("net', "require('net", 'require("tls', "require('tls",
186
+ 'require("dgram', "require('dgram", 'require("vm', "require('vm",
187
+ // DYNAMIC_CODE
188
+ 'eval(', 'new function', 'function(', 'vm.runincontext', 'vm.runinnewcontext',
189
+ 'vm.runinthiscontext', 'runinnewcontext', 'vm.script(', 'settimeout(', 'setinterval(', '(0,eval)',
190
+ // CODE_DECODE
191
+ 'atob(', 'buffer.from',
192
+ // PROCESS_EXEC
193
+ 'child_process', 'execsync(', 'spawnsync(', 'execfile(', 'execfilesync(', 'shellstring',
194
+ 'process.binding(',
195
+ // DYNAMIC_REQUIRE (unambiguous forms; bare require(var) is checked separately)
196
+ 'require.resolve(', 'module._load',
197
+ ];
198
+
199
+ const BEHAVIOR_PRESCREENER_KEYWORDS_NO_WS = BEHAVIOR_PRESCREENER_KEYWORDS.map(kw => kw.replace(/\s+/g, ''));
200
+
158
201
  // A quoted absolute URL passed directly as the argument of an actual network-call site --
159
202
  // distinguishes theft (hardcodes the destination) from legit npm tooling (builds the URL
160
203
  // from config, e.g. `fetch(`${registry}/${name}`)`). Anchored to the call site itself (not
@@ -183,6 +226,10 @@ class BehaviorTracker {
183
226
  this.filePackage = new Map();
184
227
  // Accumulated violations for telemetry
185
228
  this.violations = [];
229
+ // Fast-path pre-screener: single Aho-Corasick over all signal literal substrings.
230
+ this._prescreener = new AhoCorasick(BEHAVIOR_PRESCREENER_KEYWORDS);
231
+ // Whitespace-normalized pre-screener catches spaced variants like `fetch (...)`.
232
+ this._prescreenerNoWs = new AhoCorasick(BEHAVIOR_PRESCREENER_KEYWORDS_NO_WS);
186
233
  }
187
234
 
188
235
  /**
@@ -193,6 +240,36 @@ class BehaviorTracker {
193
240
  analyzeModule(filename, content, packageKey) {
194
241
  if (!content) return [];
195
242
 
243
+ const contentNoWs = content.replace(/\s+/g, '');
244
+
245
+ // Fast-path: if no signal keyword is present in either the raw content or a
246
+ // whitespace-collapsed variant, all regex-based signal checks are guaranteed to be false
247
+ // (the pre-screener is a superset of all SIGNAL_PATTERN regexes). Skip the expensive
248
+ // scanSrc normalization chain and 60+ regex tests. DYNAMIC_REQUIRE is the one signal
249
+ // without an unambiguous literal keyword (require(var) vs require('literal')), so it is
250
+ // checked separately.
251
+ if (!this._prescreener.searchInsensitive(content) &&
252
+ !this._prescreenerNoWs.searchInsensitive(contentNoWs)) {
253
+ const dynamicRequire = matchesAny(content, SIGNAL_PATTERNS.DYNAMIC_REQUIRE);
254
+ const signals = {
255
+ sensitiveRead: false, sensitivePath: false, sensitiveConfigPath: false,
256
+ npmrcRead: false, npmrcToken: false, hostOption: false,
257
+ hardcodedEgress: false, hardcodedEgressNonRegistry: false,
258
+ envRead: false, networkEgress: false, dynamicCode: false,
259
+ codeDecode: false, processExec: false, dynamicRequire,
260
+ };
261
+ this.moduleSignals.set(filename, signals);
262
+ if (packageKey !== undefined && packageKey !== null) this.filePackage.set(filename, packageKey);
263
+ if (!dynamicRequire) return [];
264
+ const found = [{
265
+ rule: 'DYNAMIC_MODULE_LOAD',
266
+ severity: 'MEDIUM',
267
+ description: 'Module uses dynamic require() or module._load with a non-literal path',
268
+ }];
269
+ this.violations.push({ filename, violations: found, timestamp: Date.now() });
270
+ return found;
271
+ }
272
+
196
273
  // SENSITIVE_PATH / SENSITIVE_READ must only fire on genuine filesystem access, not on
197
274
  // import/require module specifiers (e.g. "@memberjunction/credentials") or URL paths
198
275
  // (e.g. "https://api.example.com/totpSecret"). Blank just the specifier STRING in place
@@ -452,4 +529,4 @@ class BehaviorTracker {
452
529
 
453
530
  // SIGNAL_PATTERNS is exported so downstream tooling can iterate the raw signal regexes for
454
531
  // evidence reconstruction (the registry's watch-changes.js). Keeps this engine a drop-in copy.
455
- module.exports = { BehaviorTracker, SIGNAL_PATTERNS };
532
+ module.exports = { BehaviorTracker, SIGNAL_PATTERNS, BEHAVIOR_PRESCREENER_KEYWORDS };