aletheia-firewall 0.3.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.
@@ -0,0 +1 @@
1
+ 800c13e8709801a5cd30d832a05d9e1fe0fd51780af348860c35a430d92957c4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Holeyfield33-art
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 ADDED
@@ -0,0 +1,129 @@
1
+ # Aletheia Firewall
2
+
3
+ Zero-dependency runtime firewall that blocks malicious npm modules at require-time through behavioral detection, Aho-Corasick signature scanning, and policy enforcement.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install aletheia-firewall
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ FW_ENABLE_DETECTION=1 node --require aletheia-firewall app.js
15
+ ```
16
+
17
+ For Bun:
18
+
19
+ ```bash
20
+ FW_ENABLE_DETECTION=1 BUN_PRELOAD=aletheia-firewall bun app.js
21
+ ```
22
+
23
+ ## Environment Variables
24
+
25
+ | Variable | Default | Description |
26
+ |----------|---------|-------------|
27
+ | `FW_ENABLE_DETECTION` | `0` | Set to `1` to activate the firewall (required) |
28
+ | `FW_ENABLE_BEHAVIORAL` | `1` | Set to `0` to disable the behavioral pass while keeping signature scanning active. Useful as an escape hatch if behavioral detection produces false positives. Note: several detections (credential exfiltration, dynamic-code/exec chains, base64→eval obfuscation) rely on the behavioral pass — disabling it falls back to signature-only coverage. |
29
+ | `FW_TELEMETRY` | `0` | Set to `1` to start a telemetry worker that POSTs events to `FW_CONTROL_PORT`; with no control plane running it fails open and delivers nothing. |
30
+ | `FW_CONTROL_PORT` | `3000` | Port for the control plane telemetry ingestion endpoint (`fw-control`). Used by the telemetry worker when `FW_TELEMETRY=1`. |
31
+ | `FW_STRICT_PRELOAD` | `0` | Set to `1` to exit if not loaded via `--require` |
32
+ | `FW_FREEZE_PROTOTYPES` | `0` | Set to `1` to freeze built-in prototypes (prototype-pollution hardening; opt-in because it breaks some polyfills and test frameworks) |
33
+ | `FW_POLICY_PUBKEY` | *(dev key)* | PEM-encoded Ed25519 SPKI public key for verifying `policy.signed.json`. **Must be set in production** — the bundled dev private key is public. |
34
+ | `FW_ALLOW_DEV_POLICY_KEY` | `0` | Set to `1` to allow the dev key when `FW_POLICY_PUBKEY` is unset (local/dev/CI). Agent refuses to start with a policy file present and no production key unless this flag is set. |
35
+ | `HELIOS_LOG_DIR` | `/var/log/helios` | Audit log directory |
36
+ | `HELIOS_BLOCK_SCRIPTS` | `1` | Set to `0` to warn instead of block suspicious npm scripts |
37
+ | `BUN_PRELOAD` | *(none)* | Must include `aletheia-firewall` when running under Bun; the agent exits with code 1 if absent |
38
+ | `DENO_PRELOAD` | *(none)* | Must include `aletheia-firewall` when running under Deno; the agent exits with code 1 if absent |
39
+
40
+ > Telemetry is **off by default**. `FW_TELEMETRY=1` starts a telemetry worker that POSTs events to the control plane at `FW_CONTROL_PORT`. The control plane (`fw-control`) ships in this repo and can be started with `npm run start:control`.
41
+
42
+ ## Policy File
43
+
44
+ `policy.signed.json` must be a **signed envelope** (`{ version, rules, signedAt, signature }`) —
45
+ an unsigned `{ "rules": … }` object fails verification on startup and triggers emergency lockdown.
46
+ Author a plain rules file and sign it:
47
+
48
+ ```bash
49
+ echo '{ "malware.js": "BLOCK", "untrusted-pkg.js": "QUARANTINE", "noisy-lib.js": "OBSERVE" }' > rules.json
50
+ node scripts/sign-policy.js scripts/dev-private-key.pem rules.json policy.signed.json
51
+ ```
52
+
53
+ For the bundled dev key you must run with `FW_ALLOW_DEV_POLICY_KEY=1`; in production sign with your
54
+ own key (`scripts/generate-policy-key.js`) and set `FW_POLICY_PUBKEY`.
55
+
56
+ - **BLOCK**: Module never runs.
57
+ - **QUARANTINE**: Exports replaced with a logging Proxy; child requires blocked.
58
+ - **OBSERVE** (default): Full behavioral + signature scan; blocks on detection.
59
+
60
+ > **Signing:** `policy.signed.json` carries a real **Ed25519 signature** over its canonical
61
+ > payload `{ version, rules (keys sorted), signedAt }`, re-verified every 60 seconds. An invalid
62
+ > or missing signature triggers emergency lockdown. The verifying public key is compiled into
63
+ > `src/policy-watcher.js` and overridable via `FW_POLICY_PUBKEY`; author a rules file and sign it
64
+ > with `scripts/sign-policy.js`. (Since v0.2.0 this replaced the earlier SHA-256 trust-on-first-use
65
+ > baseline — earlier docs describing SHA-256-only monitoring are obsolete.)
66
+
67
+ ## Performance
68
+
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).
70
+
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:
72
+
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 |
77
+
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%.
79
+
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.
81
+
82
+ To reproduce, run the 900-module gate from the GitHub repo: `npm run gate`.
83
+
84
+ ## Known Bypasses
85
+
86
+ This firewall provides defense-in-depth but cannot catch all threats. Documented bypasses require dynamic or AST-level analysis:
87
+
88
+ | Technique | Status |
89
+ |-----------|--------|
90
+ | Direct `eval("code")` + exec | **BLOCKED** (behavioral `DYNAMIC_CODE_EXEC_CHAIN`) |
91
+ | `Buffer.from(b64,'base64').toString() -> eval` | **BLOCKED** (behavioral `OBFUSCATED_CODE_EXECUTION`; bare `buffer.from`/`eval(` are WARN-only, the decode+eval combination blocks) |
92
+ | `atob`/hex-decode -> `new Function` | **BLOCKED** (behavioral `OBFUSCATED_CODE_EXECUTION`) |
93
+ | Crypto-miner stratum URL | **BLOCKED** |
94
+ | `.env`/credential read + network call | **BLOCKED** |
95
+ | `eval` + `child_process.exec` | **BLOCKED** |
96
+ | `curl \| bash` in host project's npm scripts | **BLOCKED** (root scripts only; not dependency install hooks) |
97
+ | Bracket eval: `this["ev"+"al"]` | **BYPASSES** — needs AST analysis |
98
+ | String concat: `global["ev"+"al"]` | **BYPASSES** — needs taint tracking |
99
+ | Variable-alias eval: `const fn = eval; fn("code")` | **BYPASSES** — needs runtime Proxy / taint tracking |
100
+ | Array join: `["ch","ild"].join("")` | **BYPASSES (per-module)** — may be caught by cross-module state |
101
+ | Prototype chain: `eval.constructor` | **BYPASSES** — needs runtime instrumentation |
102
+
103
+ See the monorepo's `docs/THREAT-COVERAGE.md` for the full, test-backed protection/bypass matrix.
104
+
105
+ ## Behavioral Detection Notes
106
+
107
+ - **`process.env` + network egress is intentionally NOT blocked** (WARN only): it is the everyday
108
+ pattern legitimate analytics/telemetry SDKs use. Only a genuine credential *path* (`.env`, `.ssh`,
109
+ `id_rsa`, …) or `.npmrc` token/host/hardcoded-exfil signal escalates to a CRITICAL block.
110
+ - **Dynamic `require(variable)`** surfaces as an `OBSERVE`/telemetry signal, not a block —
111
+ non-literal `require` is pervasive in legitimate code (lazy loading, plugin systems).
112
+
113
+ ## Tests
114
+
115
+ Tests live in the monorepo root — they are not included in the published package. To run them:
116
+
117
+ ```bash
118
+ git clone https://github.com/holeyfield33-art/runtime-firewall-mvp
119
+ cd runtime-firewall-mvp
120
+ npm install
121
+ npm run test:unit # Aho-Corasick + Detector
122
+ npm run test:adversarial # adversarial bypass cases
123
+ npm run test:coverage # engine-core coverage gate (95%)
124
+ npm test # all
125
+ ```
126
+
127
+ ## License
128
+
129
+ MIT
package/index.js ADDED
@@ -0,0 +1,383 @@
1
+ // packages/fw-agent/index.js
2
+ const Module = require('module');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const crypto = require('crypto');
6
+ const { Worker } = require('worker_threads');
7
+
8
+ // Exit early and export nothing if detection is not enabled - zero overhead for baseline runs
9
+ if (process.env.FW_ENABLE_DETECTION !== '1') {
10
+ module.exports = {};
11
+ return;
12
+ }
13
+
14
+ const { Detector } = require('./src/detector');
15
+ const { QuarantineStub } = require('./src/quarantine');
16
+ const { PolicyWatcher, assertProductionKeyConfig } = require('./src/policy-watcher');
17
+ const { getAuditLog } = require('./src/audit-log');
18
+
19
+ // ── Runtime detection: fail closed if running under Bun or Deno without preload ──────────────
20
+ (function detectRuntime() {
21
+ if (typeof process.versions.bun !== 'undefined') {
22
+ const preload = process.env.BUN_PRELOAD || '';
23
+ if (!preload.includes('aletheia-firewall') && !preload.includes('fw-agent') && !preload.includes('helios')) {
24
+ console.error('[CRITICAL] Helios is not preloaded in Bun runtime. Set BUN_PRELOAD=aletheia-firewall. Exiting.');
25
+ process.exit(1);
26
+ }
27
+ }
28
+ if (typeof process.versions.deno !== 'undefined') {
29
+ const preload = process.env.DENO_PRELOAD || '';
30
+ if (!preload.includes('aletheia-firewall') && !preload.includes('fw-agent') && !preload.includes('helios')) {
31
+ console.error('[CRITICAL] Helios is not preloaded in Deno runtime. Exiting.');
32
+ process.exit(1);
33
+ }
34
+ }
35
+ })();
36
+
37
+ // ── Preload verification ──────────────────────────────────────────────────────────────────────
38
+ // Strict mode (FW_STRICT_PRELOAD=1) exits if agent was not injected via --require.
39
+ // Default mode warns so programmatic loading (and tests) still work.
40
+ //
41
+ // Detection parses process.execArgv for an actual --require / -r flag whose value resolves
42
+ // to THIS agent module. The earlier implementation did a substring search over the joined
43
+ // execArgv for "fw-agent"/"helios"/"aletheia-firewall" — trivially spoofed: `node -e
44
+ // "require('./packages/fw-agent')"` puts the whole inline script (containing "fw-agent")
45
+ // into execArgv, so the check reported "preloaded" and silently no-op'd, defeating the very
46
+ // guarantee it exists to enforce. We now require a genuine preload flag pointing at us.
47
+ (function verifyPreloadManifold() {
48
+ const execArgv = process.execArgv || [];
49
+
50
+ // Resolve a --require/-r value the same way Node would (relative to cwd), then compare its
51
+ // resolved module path to this agent. A failure to resolve is simply "not us".
52
+ const resolvesToAgent = (value) => {
53
+ if (!value) return false;
54
+ try {
55
+ const resolved = require.resolve(value, { paths: [process.cwd()] });
56
+ // __dirname is packages/fw-agent; index.js (this file) is the package entry point.
57
+ return resolved === __filename || resolved.startsWith(__dirname + path.sep);
58
+ } catch (e) {
59
+ // Bare specifier form (e.g. --require aletheia-firewall) that can't be resolved from
60
+ // cwd here still counts if it names this package.
61
+ return /(?:^|[\\/])(?:aletheia-firewall|fw-agent)(?:[\\/]|$)/.test(value);
62
+ }
63
+ };
64
+
65
+ let isPreloaded = false;
66
+ for (let i = 0; i < execArgv.length; i++) {
67
+ const arg = execArgv[i];
68
+ if (arg === '--require' || arg === '-r') {
69
+ if (resolvesToAgent(execArgv[i + 1])) { isPreloaded = true; break; }
70
+ } else if (arg.startsWith('--require=') || arg.startsWith('-r=')) {
71
+ if (resolvesToAgent(arg.slice(arg.indexOf('=') + 1))) { isPreloaded = true; break; }
72
+ }
73
+ }
74
+
75
+ if (!isPreloaded) {
76
+ if (process.env.FW_STRICT_PRELOAD === '1') {
77
+ console.error('[CRITICAL] Helios was not injected via --require. Set --require=aletheia-firewall to ensure all modules are intercepted from startup. Exiting.');
78
+ process.exit(1);
79
+ } else {
80
+ console.warn('[Helios] Warning: agent loaded via require() rather than --require. Modules loaded before this point are not protected.');
81
+ }
82
+ }
83
+ })();
84
+
85
+ // ── Primitive prototype lockdown (opt-in via FW_FREEZE_PROTOTYPES=1) ───────────────────────────
86
+ // Disabled by default: freezing built-in prototypes breaks legitimate libraries
87
+ // (older polyfills, some ORMs, test frameworks) with confusing downstream errors.
88
+ // Set FW_FREEZE_PROTOTYPES=1 to enable. See F-11 in the security audit.
89
+ (function primitiveLockdown() {
90
+ if (process.env.FW_FREEZE_PROTOTYPES !== '1') return;
91
+ const intrinsicPrototypes = [Object.prototype, Array.prototype, Function.prototype, Promise.prototype, RegExp.prototype];
92
+ for (const proto of intrinsicPrototypes) {
93
+ try {
94
+ Object.freeze(proto);
95
+ Object.getOwnPropertyNames(proto).forEach(prop => {
96
+ try { Object.defineProperty(proto, prop, { writable: false, configurable: false }); } catch (e) {}
97
+ });
98
+ } catch (e) {}
99
+ }
100
+ })();
101
+
102
+ // ── Self-integrity check ──────────────────────────────────────────────────────────────────────
103
+ (function verifySelfIntegrity() {
104
+ const baselineFile = path.join(__dirname, '.helios-baseline');
105
+ const selfFiles = [
106
+ path.join(__dirname, 'index.js'),
107
+ path.join(__dirname, 'src', 'detector.js'),
108
+ path.join(__dirname, 'src', 'behavior-tracker.js'),
109
+ path.join(__dirname, 'src', 'policy-watcher.js'),
110
+ path.join(__dirname, 'src', 'quarantine.js'),
111
+ path.join(__dirname, 'src', 'audit-log.js'),
112
+ path.join(__dirname, 'src', 'policy.js'),
113
+ ];
114
+
115
+ function computeSelfHash() {
116
+ const hash = crypto.createHash('sha256');
117
+ for (const f of selfFiles) {
118
+ try {
119
+ const content = fs.readFileSync(f, 'utf8').replace(/\r\n/g, '\n');
120
+ hash.update(content, 'utf8');
121
+ } catch (e) {}
122
+ }
123
+ return hash.digest('hex');
124
+ }
125
+
126
+ if (fs.existsSync(baselineFile)) {
127
+ const stored = fs.readFileSync(baselineFile, 'utf8').trim();
128
+ const current = computeSelfHash();
129
+ if (stored !== current) {
130
+ console.error('[CRITICAL] Firewall self-integrity check FAILED. Helios code has been tampered with. Refusing to run.');
131
+ process.exit(1);
132
+ }
133
+ } else {
134
+ // Baseline is committed to the repo and shipped in the npm manifest.
135
+ // A missing baseline means the file was deleted or the package was tampered with.
136
+ // Never silently re-baseline — fail closed so the operator knows something is wrong.
137
+ console.error('[CRITICAL] Firewall self-integrity baseline (.helios-baseline) is missing. Cannot verify agent integrity. Refusing to run.');
138
+ process.exit(1);
139
+ }
140
+ })();
141
+
142
+ // ── Production policy-key sanity check (F-33) ──────────────────────────────────────────────────
143
+ // Runs regardless of whether a policy.signed.json exists on disk. Refuses to start in
144
+ // production when the bundled (public) dev key would be used to verify policies.
145
+ assertProductionKeyConfig();
146
+
147
+ // ── npm lifecycle script scanning ────────────────────────────────────────────────────────────
148
+ (function scanNpmLifecycleScripts() {
149
+ const pkgPath = path.join(process.cwd(), 'package.json');
150
+ if (!fs.existsSync(pkgPath)) return;
151
+
152
+ const SUSPICIOUS_SCRIPT_PATTERNS = [
153
+ /curl\s+.*\|\s*(ba)?sh/i,
154
+ /wget\s+.*\|\s*(ba)?sh/i,
155
+ /node\s+.*download/i,
156
+ /python\s+.*http/i,
157
+ /bash\s+-c\s+['"]/i,
158
+ /eval\s*\$/i,
159
+ /base64\s+--decode/i,
160
+ ];
161
+
162
+ let pkg;
163
+ try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch (e) { return; }
164
+ if (!pkg.scripts) return;
165
+
166
+ for (const [scriptName, cmd] of Object.entries(pkg.scripts)) {
167
+ if (typeof cmd !== 'string') continue;
168
+ if (SUSPICIOUS_SCRIPT_PATTERNS.some(p => p.test(cmd))) {
169
+ console.error(`[HELIOS] Suspicious npm lifecycle script blocked: "${scriptName}" = "${cmd}"`);
170
+ getAuditLog().write({ eventType: 'SUSPICIOUS_SCRIPT', scriptName, command: cmd });
171
+ if (process.env.HELIOS_BLOCK_SCRIPTS !== '0') {
172
+ process.exit(1);
173
+ }
174
+ }
175
+ }
176
+ })();
177
+
178
+ // ── Telemetry worker thread ───────────────────────────────────────────────────────────────────
179
+ const telemetryEnabled = process.env.FW_TELEMETRY === '1';
180
+ const telemetryWorkerPath = path.join(__dirname, 'sync-worker.js');
181
+ const telemetryWorker = telemetryEnabled ? (() => {
182
+ const w = new Worker(telemetryWorkerPath);
183
+ w.unref();
184
+ return w;
185
+ })() : null;
186
+
187
+ // ── Audit log (persistent) ────────────────────────────────────────────────────────────────────
188
+ const auditLog = getAuditLog();
189
+
190
+ // ── Policy loading & continuous integrity watcher ────────────────────────────────────────────
191
+ let policyMap = new Map();
192
+ const POLICY_PATH = path.join(process.cwd(), 'policy.signed.json');
193
+
194
+ // Build a policyMap from a rules object (called on startup and on hot-reload).
195
+ function buildPolicyMap(rules) {
196
+ return new Map(Object.entries(rules || {}));
197
+ }
198
+
199
+ // Emergency lockdown: block ALL module loads
200
+ let emergencyLockdown = false;
201
+
202
+ // PolicyWatcher verifies the Ed25519 signature on every interval tick.
203
+ // onTamperDetected → invalid/missing signature → lockdown
204
+ // onValidChange → valid signature + new rules → hot-reload policyMap
205
+ const policyWatcher = new PolicyWatcher(POLICY_PATH, {
206
+ onTamperDetected: () => {
207
+ emergencyLockdown = true;
208
+ auditLog.write({ eventType: 'POLICY_TAMPER_LOCKDOWN', timestamp: Date.now() });
209
+ emitTelemetry('POLICY_TAMPER_LOCKDOWN', 'policy.signed.json', null);
210
+ },
211
+ onValidChange: (rules) => {
212
+ policyMap = buildPolicyMap(rules);
213
+ },
214
+ });
215
+ policyWatcher.start();
216
+
217
+ // ── Detector ─────────────────────────────────────────────────────────────────────────────────
218
+ const detector = new Detector(policyMap);
219
+
220
+ // ── Telemetry helpers ─────────────────────────────────────────────────────────────────────────
221
+ function emitTelemetry(eventType, packageName, parentPackage, metadata = {}) {
222
+ if (!telemetryWorker) return;
223
+ telemetryWorker.postMessage({
224
+ type: 'TELEMETRY_EVENT',
225
+ payload: { eventType, packageName, parentPackage, timestamp: Date.now(), ...metadata },
226
+ });
227
+ }
228
+
229
+ // ── Compilation metrics ───────────────────────────────────────────────────────────────────────
230
+ const compileMetrics = { filesCompiled: 0, lockdownsEnforced: 0, quarantined: 0 };
231
+ // Cache keyed by filename → SHA-256 of content (not filename alone).
232
+ // Re-scans the file if its content changed between require() calls in a long-lived process.
233
+ const verifiedCompilationsCache = new Map();
234
+ const quarantinedModules = new Set();
235
+
236
+ // ── Core module interception hook ─────────────────────────────────────────────────────────────
237
+ const originalCompile = Module.prototype._compile;
238
+
239
+ // Derive the npm-package key for a filename so cross-file correlation stays scoped to ONE
240
+ // package. The behavioral tracker is reset per dependency-tree root (below), which spans the
241
+ // whole app — without this scoping, cross-file rules would pair a config-reading module with any
242
+ // unrelated http module in the tree and false-positive. Returns null for first-party app code
243
+ // (no node_modules segment): the developer's own files reading config and making network calls
244
+ // across files is normal, not the split-attack threat model, so cross-file is skipped for them.
245
+ function packageKeyForFilename(filename) {
246
+ const norm = String(filename).replace(/\\/g, '/');
247
+ const idx = norm.lastIndexOf('/node_modules/');
248
+ if (idx === -1) return null;
249
+ const rest = norm.slice(idx + '/node_modules/'.length).split('/');
250
+ if (rest[0] && rest[0][0] === '@') return rest[0] + '/' + (rest[1] || '');
251
+ return rest[0] || null;
252
+ }
253
+
254
+ Module.prototype._compile = function (content, filename) {
255
+ // Reset cross-module behavioral state at each new dependency-tree root so that
256
+ // benign modules in one tree cannot poison detection in an unrelated tree.
257
+ if (this.parent === null) {
258
+ detector.behaviorTracker.reset();
259
+ }
260
+
261
+ // Emergency lockdown: block everything
262
+ if (emergencyLockdown) {
263
+ throw new Error('[Firewall] Emergency lockdown active. All module loads blocked.');
264
+ }
265
+
266
+ // Block loads initiated by a quarantined module
267
+ if (this.parent && quarantinedModules.has(this.parent.filename)) {
268
+ const requestName = path.basename(filename);
269
+ const event = { eventType: 'QUARANTINE_BLOCK_REQUIRE', blockedModule: requestName, origin: path.basename(this.parent.filename), timestamp: Date.now() };
270
+ auditLog.write(event);
271
+ emitTelemetry('QUARANTINE_BLOCK_REQUIRE', requestName, path.basename(this.parent.filename));
272
+ throw new Error(`[Firewall] Quarantined module "${path.basename(this.parent.filename)}" cannot load "${requestName}"`);
273
+ }
274
+
275
+ const requestName = path.basename(filename);
276
+ const configuredRule = policyMap.get(requestName) || 'OBSERVE';
277
+
278
+ if (configuredRule === 'BLOCK') {
279
+ const event = { eventType: 'BLOCK', packageName: requestName, timestamp: Date.now() };
280
+ auditLog.write(event);
281
+ emitTelemetry('BLOCK', requestName, null);
282
+ throw new Error(`[Firewall] Compilation denied for module: "${requestName}"`);
283
+ }
284
+
285
+ if (configuredRule === 'QUARANTINE') {
286
+ compileMetrics.quarantined++;
287
+ const event = { eventType: 'QUARANTINE_ACTIVE', packageName: requestName, source: 'policy', timestamp: Date.now() };
288
+ auditLog.write(event);
289
+ emitTelemetry('QUARANTINE_ACTIVE', requestName, null, { source: 'policy' });
290
+ quarantinedModules.add(filename);
291
+ // Return a stub without executing the module's code
292
+ const stub = new QuarantineStub(requestName, { emit: (t, d) => emitTelemetry(t, requestName, null, d) });
293
+ this.exports = stub.createProxy();
294
+ return;
295
+ }
296
+
297
+ if (configuredRule === 'OBSERVE') {
298
+ const contentHash = crypto.createHash('sha256').update(content).digest('hex');
299
+ if (verifiedCompilationsCache.get(filename) === contentHash) {
300
+ return originalCompile.apply(this, arguments);
301
+ }
302
+
303
+ compileMetrics.filesCompiled++;
304
+ const scanResult = detector.scanModuleSync(requestName, content, filename, packageKeyForFilename(filename));
305
+
306
+ // Split block-tier detections from WARN-only observations. WARN-tier matches (e.g.
307
+ // https.request, buffer.from) and MEDIUM behavioral findings never reach blockDetections:
308
+ // the detector marks anything below HIGH as warnOnly (see detector.js — only CRITICAL/HIGH
309
+ // behavioral violations are pushed as non-warnOnly). So blockDetections holds exactly the
310
+ // HIGH/CRITICAL findings, which hard-block. DYNAMIC_MODULE_LOAD (MEDIUM, require(variable))
311
+ // is intentionally NOT quarantined here — non-literal require() is pervasive in legitimate
312
+ // code (lazy loads, plugin systems, require(path.join(...))), so it surfaces as an OBSERVE
313
+ // telemetry signal only. (F-34: removed a dead `hasMediumOnly` quarantine branch that could
314
+ // never fire because no non-warnOnly MEDIUM detection is ever produced.)
315
+ const blockDetections = scanResult.detections.filter(d => !d.warnOnly);
316
+ const warnDetections = scanResult.detections.filter(d => d.warnOnly);
317
+
318
+ if (warnDetections.length > 0) {
319
+ emitTelemetry('OBSERVE', requestName, null, { warnMatches: warnDetections.map(d => d.matched) });
320
+ }
321
+
322
+ if (blockDetections.length > 0) {
323
+ compileMetrics.lockdownsEnforced++;
324
+ const event = {
325
+ eventType: 'DETECTION_TRIGGERED',
326
+ packageName: requestName,
327
+ detections: blockDetections,
328
+ timestamp: Date.now(),
329
+ };
330
+ auditLog.write(event);
331
+ emitTelemetry('DETECTION_TRIGGERED', requestName, null, { detections: blockDetections });
332
+
333
+ const msg = `[Firewall] Detection in "${requestName}": ${blockDetections.map(d => d.rule || d.type).join(', ')}`;
334
+ console.error(`\n[COMPILATION LOCKDOWN] Threat detected in "${requestName}"`);
335
+ throw new Error(msg);
336
+ }
337
+
338
+ verifiedCompilationsCache.set(filename, contentHash);
339
+ }
340
+
341
+ return originalCompile.apply(this, arguments);
342
+ };
343
+
344
+ // ── Graceful shutdown ─────────────────────────────────────────────────────────────────────────
345
+ async function shutdown(signal) {
346
+ console.log(`\n[Helios] Received ${signal}. Flushing telemetry and shutting down workers...`);
347
+
348
+ policyWatcher.stop();
349
+
350
+ if (telemetryWorker) {
351
+ telemetryWorker.postMessage({ type: 'FORCE_FLUSH' });
352
+ // Give the worker a moment to flush before terminating
353
+ await new Promise(resolve => setTimeout(resolve, 500));
354
+ try { await telemetryWorker.terminate(); } catch (e) {}
355
+ }
356
+
357
+ auditLog.write({ eventType: 'AGENT_SHUTDOWN', signal, timestamp: Date.now() });
358
+ auditLog.close();
359
+
360
+ console.log(`[Helios] Shutdown complete. Monitored: ${compileMetrics.filesCompiled}, Quarantined: ${compileMetrics.quarantined}, Blocked: ${compileMetrics.lockdownsEnforced}`);
361
+ }
362
+
363
+ process.on('SIGTERM', () => shutdown('SIGTERM').then(() => process.exit(0)));
364
+ process.on('SIGINT', () => shutdown('SIGINT').then(() => process.exit(0)));
365
+
366
+ process.on('exit', (code) => {
367
+ if (code !== 9) {
368
+ console.log(`\n[Helios] Exit ${code} | Compilations: ${compileMetrics.filesCompiled} | Quarantined: ${compileMetrics.quarantined} | Blocked: ${compileMetrics.lockdownsEnforced}`);
369
+ }
370
+ if (telemetryWorker) {
371
+ telemetryWorker.postMessage({ type: 'FORCE_FLUSH' });
372
+ }
373
+ // Sync close - safe on exit event
374
+ try { auditLog.close(); } catch (e) {}
375
+ });
376
+
377
+ // Log startup
378
+ auditLog.write({ eventType: 'AGENT_START', timestamp: Date.now(), logPath: auditLog.filePath });
379
+
380
+ // Export via getter so consumers always see the live map after hot-reload (F-21).
381
+ const _exports = { compileMetrics, quarantinedModules };
382
+ Object.defineProperty(_exports, 'policyMap', { get: () => policyMap, enumerable: true });
383
+ module.exports = _exports;
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "aletheia-firewall",
3
+ "version": "0.3.0",
4
+ "description": "Zero-dependency runtime firewall that blocks malicious npm modules at require-time.",
5
+ "main": "index.js",
6
+ "license": "MIT",
7
+ "author": "Aletheia contributors",
8
+ "homepage": "https://github.com/holeyfield33-art/runtime-firewall-mvp#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/holeyfield33-art/runtime-firewall-mvp/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/holeyfield33-art/runtime-firewall-mvp.git"
15
+ },
16
+ "keywords": [
17
+ "security",
18
+ "supply-chain",
19
+ "npm",
20
+ "firewall",
21
+ "runtime",
22
+ "malware",
23
+ "shai-hulud"
24
+ ],
25
+ "files": [
26
+ "index.js",
27
+ "src/",
28
+ "sync-worker.js",
29
+ ".helios-baseline",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ }
36
+ }
@@ -0,0 +1,101 @@
1
+ class AhoCorasick {
2
+ constructor(keywords) {
3
+ // Use array-based transitions (indexed by charCode 0-127) for O(1) hot-path access
4
+ // without string-keyed object property lookups or per-char allocations.
5
+ this.trie = { next: new Array(128).fill(null), fail: null, output: null };
6
+ this._buildTrie(keywords);
7
+ this._buildFailureLinks();
8
+ }
9
+
10
+ _buildTrie(keywords) {
11
+ for (const kw of keywords) {
12
+ let node = this.trie;
13
+ for (const ch of kw.toLowerCase()) {
14
+ const code = ch.charCodeAt(0) & 0x7f; // ASCII
15
+ if (!node.next[code]) {
16
+ node.next[code] = { next: new Array(128).fill(null), fail: null, output: null };
17
+ }
18
+ node = node.next[code];
19
+ }
20
+ node.output = kw;
21
+ }
22
+ }
23
+
24
+ _buildFailureLinks() {
25
+ const queue = [];
26
+ for (let code = 0; code < 128; code++) {
27
+ const child = this.trie.next[code];
28
+ if (child) {
29
+ child.fail = this.trie;
30
+ queue.push(child);
31
+ }
32
+ }
33
+
34
+ while (queue.length > 0) {
35
+ const node = queue.shift();
36
+ for (let code = 0; code < 128; code++) {
37
+ const child = node.next[code];
38
+ if (!child) continue;
39
+ let fail = node.fail;
40
+ while (fail && !fail.next[code]) {
41
+ fail = fail.fail;
42
+ }
43
+ child.fail = fail ? fail.next[code] : this.trie;
44
+ child.output = child.output || (child.fail && child.fail.output) || null;
45
+ queue.push(child);
46
+ }
47
+ }
48
+ }
49
+
50
+ search(text) {
51
+ if (!text || typeof text !== 'string') return null;
52
+ // Assumes caller has normalized text (lowercased) to avoid per-character toLowerCase
53
+ let node = this.trie;
54
+ const trie = this.trie;
55
+ for (let i = 0; i < text.length; i++) {
56
+ let code = text.charCodeAt(i) & 0x7f;
57
+ while (node !== trie && !node.next[code]) {
58
+ node = node.fail;
59
+ }
60
+ const nxt = node.next[code];
61
+ if (nxt) {
62
+ node = nxt;
63
+ }
64
+ if (node.output) {
65
+ return node.output; // return matched keyword
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * searchInsensitive - case-insensitive scan using charCode indexing into
73
+ * dense arrays. Folds A-Z to a-z inline, no string allocations or object
74
+ * property lookups in the inner loop. This is the hot path used by Detector.
75
+ */
76
+ searchInsensitive(text) {
77
+ if (!text || typeof text !== 'string') return null;
78
+ const trie = this.trie;
79
+ let node = trie;
80
+ const len = text.length;
81
+ for (let i = 0; i < len; i++) {
82
+ let code = text.charCodeAt(i);
83
+ // Fold uppercase ASCII letters to lowercase range, mask to 0-127
84
+ if (code >= 65 && code <= 90) code += 32;
85
+ code &= 0x7f;
86
+ while (node !== trie && !node.next[code]) {
87
+ node = node.fail;
88
+ }
89
+ const nxt = node.next[code];
90
+ if (nxt) {
91
+ node = nxt;
92
+ if (node.output) {
93
+ return node.output;
94
+ }
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ }
100
+
101
+ module.exports = { AhoCorasick };