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 +1 -1
- package/README.md +30 -8
- package/package.json +1 -1
- package/src/behavior-tracker.js +78 -1
package/.helios-baseline
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
-
|
|
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 |
|
|
74
|
-
|
|
75
|
-
| Median module-compile overhead |
|
|
76
|
-
| P95 overhead |
|
|
95
|
+
| Metric | Budget | Enforced? |
|
|
96
|
+
|--------|--------|-----------|
|
|
97
|
+
| Median module-compile overhead | 25% | **Yes** |
|
|
98
|
+
| P95 overhead | 30% (informational only) | No |
|
|
77
99
|
|
|
78
|
-
|
|
100
|
+
The gate is a **regression guard**, not a performance target. If the median exceeds 25%, the change needs review.
|
|
79
101
|
|
|
80
|
-
|
|
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
package/src/behavior-tracker.js
CHANGED
|
@@ -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 };
|