@allanoricil/nrg-sentinel 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -38,6 +38,7 @@ The table below is updated automatically after each CI run on `main`.
38
38
  | 18 | Deep Stack Bypass | ✅ | `4.1.7` |
39
39
  | 19 | HTTP Route Deletion | ✅ | `4.1.7` |
40
40
  | 20 | Child Process Exec | ✅ | `4.1.7` |
41
+ | 21 | SW Fetch Interception | — | — | browser-only — verify via `start-interactive.sh` |
41
42
  | 22 | FS Read | ✅ | `4.1.7` |
42
43
  | 23 | Process Env Exfiltration | ✅ | `4.1.7` |
43
44
  | 24 | Process Exit DoS | ✅ | `4.1.7` |
@@ -50,7 +51,8 @@ The table below is updated automatically after each CI run on `main`.
50
51
  | 31 | Context Permissions | ✅ | `4.1.7` |
51
52
  | 32 | Flows Inject | ✅ | `4.1.7` |
52
53
  | 33 | Node Event Hijack | ✅ | `4.1.7` |
53
- _Last updated: 2026-03-17T08:03:19Z_
54
+ | 34 | Config Node Credentials | ✅ | `4.1.7` |
55
+ _Last updated: 2026-03-18T03:24:39Z_
54
56
  <!-- DEMO-TEST-RESULTS:END -->
55
57
 
56
58
  ## Demos
@@ -79,6 +81,7 @@ Each demo is a self-contained scenario that shows an attack against Node-RED and
79
81
  | 18 | Deep Stack Bypass | Chains anonymous wrappers to push the malicious frame outside the guard window |
80
82
  | 19 | HTTP Route Deletion | Deletes existing Express routes to disable authentication endpoints |
81
83
  | 20 | Child Process Exec | Spawns a shell command via `child_process` to execute arbitrary OS commands |
84
+ | 21 | SW Fetch Interception | Browser-only: editor script uses `fetch()` to exfiltrate data; Service Worker blocks it via the network-policy allowlist |
82
85
  | 22 | FS Read | Reads `settings.js` via `require('fs')` to extract the credential secret |
83
86
  | 23 | Process Env Exfiltration | Reads `process.env` to harvest injected secrets and API keys |
84
87
  | 24 | Process Exit DoS | Calls `process.exit()` from a message handler to kill the runtime |
@@ -91,6 +94,7 @@ Each demo is a self-contained scenario that shows an attack against Node-RED and
91
94
  | 31 | Context Permissions | Reads or writes another node's context store without a grant |
92
95
  | 32 | Flows Inject | Injects a malicious node into the running flow via the flows API |
93
96
  | 33 | Node Event Hijack | Spies on or silences another node's input handler via EventEmitter APIs |
97
+ | 34 | Config Node Credentials | Interactive: explores open / restricted / locked config-node credential access |
94
98
 
95
99
  ## Capability grants
96
100
 
@@ -208,16 +212,17 @@ If credentials were silently self-readable, `node:credentials:read` would only b
208
212
 
209
213
  #### Reading credentials from a config node referenced in its config
210
214
 
211
- Config nodes exist specifically to hold and share credentials with the consumer nodes that reference them. Sentinel opens `node:credentials:read` for config nodes by default no capability grant is needed for either the config node package or the consumer.
215
+ Config node credentials are closed by default, the same as every other node type. A consumer that needs to access `configNode.credentials` directly must either hold `node:credentials:read` in its grant list, or be listed in the config node type's `nodeTypes` entry in `.sentinel-grants.json`.
212
216
 
213
- The idiomatic pattern is for the config node to read its own credentials and expose them as plain properties:
217
+ The idiomatic pattern avoids the credential proxy entirely: the config node reads `this.credentials` in its own constructor (Sentinel only proxies nodes returned from `getNode()`, not a node's own `this`), stores the secret as a plain property, and consumers read that plain property:
214
218
 
215
219
  ```js
216
220
  // node-red-contrib-influxdb/index.js
217
221
  module.exports = function (RED) {
218
222
  function InfluxConfigNode(config) {
219
223
  RED.nodes.createNode(this, config);
220
- // Config node reads its own credentialsallowed by default (no grant needed).
224
+ // Reads this.credentials on the raw thisnot via a getNode() proxy.
225
+ // No credential cap needed because Sentinel only guards getNode() return values.
221
226
  this.token = this.credentials.token;
222
227
  this.host = config.host;
223
228
  }
@@ -225,7 +230,7 @@ module.exports = function (RED) {
225
230
 
226
231
  function InfluxWriteNode(config) {
227
232
  RED.nodes.createNode(this, config);
228
- // Consumer accesses the config node's plain property — no credential cap needed.
233
+ // Consumer reads the plain property — no credential cap needed.
229
234
  var configNode = RED.nodes.getNode(config.configId);
230
235
  this.on("input", function (msg) {
231
236
  writeToInflux(configNode.host, configNode.token, msg.payload);
@@ -237,7 +242,7 @@ module.exports = function (RED) {
237
242
  ```
238
243
 
239
244
  ```js
240
- // settings.js — no node:credentials:read needed for either package
245
+ // settings.js — no node:credentials:read needed for either package under this pattern
241
246
  sentinel: {
242
247
  allow: {
243
248
  "node-red-contrib-influxdb": ["registry:register"],
@@ -245,15 +250,11 @@ sentinel: {
245
250
  }
246
251
  ```
247
252
 
248
- If a consumer accesses `configNode.credentials.token` directly instead of a plain property, it also works without any grant the config node default rule still applies because the proxy belongs to a config node.
249
-
250
- **Why config nodes are open by default:** if consumers were required to have `node:credentials:read` to access a config node, every single package that uses any config node — influxdb-write, mqtt-out, http-request, anything that reads a username or token from a config node — would need the grant. In a real installation with a dozen contrib packages, `node:credentials:read` would appear in nearly every entry in `settings.js`, and it would cease to be a meaningful security signal. You would no longer be able to tell at a glance which packages are genuinely handling raw secrets versus simply using a config node the way Node-RED was designed.
251
-
252
- The config node pattern is a declared contract between Node-RED packages: the config node's author published it specifically to share its credentials, and the consumer's author explicitly wired to it in their node definition. An operator who installs both has implicitly accepted that relationship. Making it require an extra grant would add friction to a legitimate, universal pattern without adding meaningful security. Operators who want to restrict which packages can read a specific config node's credentials can override this default via `.sentinel-grants.json` in the userDir — see the end of this section.
253
+ If a consumer accesses `configNode.credentials.token` directly via the proxy returned by `getNode()`, that access goes through Sentinel's capability check. The consumer's package needs `node:credentials:read`, or the config node type must list it in a `nodeTypes` entry.
253
254
 
254
255
  #### Reading credentials from a node that is not a config node
255
256
 
256
- If a node reads `.credentials` from an arbitrary non-config node (one it does not reference via its own config), the **target node's package** must have `node:credentials:read` in its grant list. Sentinel captures the owning package at `createNode` time and uses it for every subsequent access through that proxy:
257
+ If a node reads `.credentials` from an arbitrary non-config node, the **accessing package** must have `node:credentials:read` in its grant list. Sentinel walks the call stack from `getNode()` to identify the calling package and checks its grants:
257
258
 
258
259
  ```js
259
260
  // node-red-contrib-reader/index.js — wants to read credentials from node-red-contrib-target
@@ -262,8 +263,8 @@ module.exports = function (RED) {
262
263
  RED.nodes.createNode(this, config);
263
264
  this.on("input", function (msg) {
264
265
  var target = RED.nodes.getNode(config.targetId);
265
- // The proxy for `target` was created with node-red-contrib-target as the
266
- // owning package. Sentinel checks that package's grants, not this one's.
266
+ // Sentinel identifies node-red-contrib-reader as the caller and checks
267
+ // its grants not the target node's owning package.
267
268
  var secret = target.credentials.secret;
268
269
  this.send(msg);
269
270
  });
@@ -273,46 +274,127 @@ module.exports = function (RED) {
273
274
  ```
274
275
 
275
276
  ```js
276
- // settings.js — the TARGET package needs node:credentials:read, not the reader
277
+ // settings.js — the ACCESSOR needs node:credentials:read, not the target's package
277
278
  sentinel: {
278
279
  allow: {
279
- "node-red-contrib-reader": ["registry:register"],
280
- "node-red-contrib-target": ["registry:register", "node:credentials:read"],
280
+ "node-red-contrib-reader": ["registry:register", "node:credentials:read"],
281
+ "node-red-contrib-target": ["registry:register"],
281
282
  },
282
283
  }
283
284
  ```
284
285
 
285
- **Why the target's package is checked, not the accessor's:** if it were the accessor's package that needed the grant, then any package granted `node:credentials:read` could call `RED.nodes.eachNode()` (with `node:list`) and iterate every node in the runtime, reading credentials from all of them in one sweep a complete credential harvest with a single grant. The target-based check prevents that: having `node:credentials:read` only gives a package access to credentials stored in its _own_ nodes. It cannot reach into another package's nodes unless that package also has the grant, which the operator would have to explicitly add.
286
-
287
- If it were the accessor's grant that mattered, `node:credentials:read` would effectively mean "read any node's credentials in the entire runtime." Instead it means "this package's nodes are authorized to handle credentials," which is a much narrower and more auditable claim.
286
+ **How the dual-axis check works:** `node:credentials:read` in the accessor's grant list (step 2) is a broad capability it lets that package read `.credentials` from any node it obtains via `getNode()`. The `nodeTypes` section in `.sentinel-grants.json` provides the complementary target-side control (step 1): the target node type's author can list exactly which caller packages are approved, granting them access without requiring a broad package grant. Either axis alone is sufficient to allow the access. If neither passes, the proxy returns `undefined` for `credentials`.
288
287
 
289
- Sentinel also looks for a `.sentinel-grants.json` file in the **userDir** (next to `settings.js`). This file is the backing store for the **Sentinel editor panel** — the Node-RED UI exposes admin API routes that read and write it directly, so operators can manage target permissions through the browser without touching `settings.js`.
288
+ Sentinel also looks for a `.sentinel-grants.json` file in the **userDir** (next to `settings.js`). This file is the backing store for the **Sentinel editor panel** — the Node-RED UI exposes admin API routes that read and write it directly, so operators can manage grants through the browser without touching `settings.js`.
290
289
 
291
290
  This separation is intentional. In hardened deployments `settings.js` is mounted read-only (the Docker section mounts it `:ro`) so it cannot be modified at runtime. `.sentinel-grants.json` lives in the writable userDir (`/data` in Docker), giving the UI panel a place to persist changes. The two files have different ownership: `settings.js` is managed by deployment tooling; `.sentinel-grants.json` is managed by the UI.
292
291
 
293
- The file grants access keyed on the **target node's type** rather than the caller's package name:
292
+ #### File format
293
+
294
+ The file has two top-level sections:
295
+
296
+ ```json
297
+ {
298
+ "packages": {
299
+ "node-red-contrib-my-package": ["registry:register", "node:credentials:read"]
300
+ },
301
+ "nodeTypes": {
302
+ "my-config-node": {
303
+ "node:credentials:read": ["node-red-contrib-trusted-consumer"]
304
+ }
305
+ }
306
+ }
307
+ ```
308
+
309
+ - **`packages`** — dynamic caller grants, equivalent to `settings.sentinel.allow`. Entries here are merged with `settings.js` at runtime. This is what the Sentinel UI writes when you add a package grant through the editor.
310
+ - **`nodeTypes`** — per-node-type target permissions. Keyed on the **target node's type**, each entry lists which caller packages are allowed to perform a given operation on nodes of that type. This is used to restrict (or explicitly allow) access to a specific node type independently of the caller's package grants.
311
+
312
+ #### Node-type permissions — real-world examples
313
+
314
+ **Grant specific packages access to a config node's credentials**
315
+
316
+ To list which packages may read a config node's credentials via `getNode(id).credentials`, add the config node type to `nodeTypes`:
317
+
318
+ ```json
319
+ {
320
+ "nodeTypes": {
321
+ "influxdb": {
322
+ "node:credentials:read": ["node-red-contrib-influxdb"]
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+ Only `node-red-contrib-influxdb` gets step-1 access via the target allowlist. Any other package without `node:credentials:read` in its grant list is blocked.
329
+
330
+ **Document that no caller is listed for a config node type**
331
+
332
+ An empty array `[]` records that no package is an approved caller via the target-side check. It has the same effect as not having an entry — neither grants step-1 access. Use it to make the intent explicit in the grants file:
294
333
 
295
334
  ```json
296
335
  {
297
- "target-node-type": {
298
- "node:credentials:read": ["node-red-contrib-reader"]
336
+ "nodeTypes": {
337
+ "my-vault-config": {
338
+ "node:credentials:read": []
339
+ }
299
340
  }
300
341
  }
301
342
  ```
302
343
 
303
- This is purely additive: if the caller is in the list, access is granted immediately. If it is not, Sentinel falls through to `settings.js` as normal the file cannot block anything that `settings.js` already allows.
344
+ No package can read credentials from `my-vault-config` nodes through the target-based check. A package that has `node:credentials:read` in its `packages` entry (either in `settings.js` or in the `packages` section of this file) can still override this — the `[]` only closes the target-based path, not the caller-based path.
345
+
346
+ **Allow multiple consumers, block all others**
347
+
348
+ ```json
349
+ {
350
+ "nodeTypes": {
351
+ "mqtt-broker": {
352
+ "node:credentials:read": [
353
+ "node-red-contrib-mqtt-in",
354
+ "node-red-contrib-mqtt-out",
355
+ "node-red-contrib-mqtt-dynamic"
356
+ ]
357
+ }
358
+ }
359
+ }
360
+ ```
304
361
 
305
- The one place it does restrict behaviour is the config node default. Normally any caller can read a config node's credentials without any grant (see above). If you add a `node:credentials:read` entry for a config node type in `.sentinel-grants.json`, that default is suppressed and access is limited to the listed callers (plus whatever `settings.js` grants):
362
+ **Restrict wire rewiring to a specific tool package**
306
363
 
307
364
  ```json
308
365
  {
309
- "my-config-node": {
310
- "node:credentials:read": ["node-red-contrib-trusted-consumer"]
366
+ "nodeTypes": {
367
+ "function": {
368
+ "node:wires:write": ["node-red-contrib-flow-manager"],
369
+ "node:wires:read": ["node-red-contrib-flow-manager", "node-red-contrib-flow-auditor"]
370
+ }
311
371
  }
312
372
  }
313
373
  ```
314
374
 
315
- With this in place, `node-red-contrib-trusted-consumer` can read `my-config-node` credentials via the permissions file, and any package that has `node:credentials:read` in `settings.js` can still read them too. Packages with neither are blocked, even though `my-config-node` is a config node.
375
+ **Complete example combining both sections**
376
+
377
+ ```json
378
+ {
379
+ "packages": {
380
+ "node-red-contrib-influxdb": ["registry:register", "node:credentials:read"],
381
+ "node-red-contrib-flow-audit": ["registry:register", "node:list"]
382
+ },
383
+ "nodeTypes": {
384
+ "influxdb": {
385
+ "node:credentials:read": ["node-red-contrib-influxdb"]
386
+ },
387
+ "mqtt-broker": {
388
+ "node:credentials:read": ["node-red-contrib-mqtt-in", "node-red-contrib-mqtt-out"]
389
+ },
390
+ "my-internal-config": {
391
+ "node:credentials:read": []
392
+ }
393
+ }
394
+ }
395
+ ```
396
+
397
+ The `nodeTypes` section is purely additive: if the caller is in the list, access is granted immediately. If it is not, Sentinel falls through to the `packages` section and `settings.js` grants as normal. An entry here cannot block a package that already holds the capability in its grants.
316
398
 
317
399
  ### Grants are per package, not per node type
318
400
 
@@ -425,6 +507,23 @@ sentinel: {
425
507
 
426
508
  This pattern is useful when multiple consumer packages need the same privileged operation: centralise it in one well-audited service package, grant only that package the capability, and consumers remain unprivileged. The service becomes the policy enforcement point — it decides what it exposes, and Sentinel enforces that nothing bypasses it.
427
509
 
510
+ ## Defense architecture
511
+
512
+ Sentinel runs inside the same Node.js process as every package it protects against — there is no sandbox, no separate process, and no OS-level isolation. Meaningful enforcement in that environment requires layered hardening techniques:
513
+
514
+ | Layer | Technique | What it closes |
515
+ |---|---|---|
516
+ | 0 — Prototype hardening | `Object.preventExtensions` on all built-in prototypes | Prototype pollution before any third-party code runs |
517
+ | 1 — Module interception | `Module._load` hook + non-configurable lock | `require()` of `fs`, `http`, `child_process`, `vm`, `worker_threads` |
518
+ | 2 — Node isolation | ES6 `Proxy` on every `getNode()` return value | Property reads, writes, and `defineProperty` on live node instances |
519
+ | 3 — Surface hardening | Guarded Express routing, `process.env` Proxy, router-stack Proxy | Post-init manipulation of the HTTP server and environment |
520
+ | 4 — Network policy | Outbound HTTP/HTTPS/socket allowlist | Exfiltration paths not covered by the module gate |
521
+ | Cross-cutting | Intrinsic capture, call-stack introspection, file integrity watchdog | Prototype mutation of guard helpers, call-identity forgery, on-disk tampering |
522
+
523
+ All built-in methods used by guard logic are pinned as standalone bound functions before the first `require()`, so a package that overwrites `String.prototype.includes` cannot blind the stack-frame checks. The `Module._load` hook is locked `configurable: false` immediately after installation so it cannot be stripped. Every node proxy intercepts `defineProperty` in addition to `get`/`set`, closing the bypass that would otherwise let a caller install a getter on a proxied node.
524
+
525
+ For the full reference — every technique explained with code examples and attack scenarios — see **[docs/defense-techniques.md](docs/defense-techniques.md)**.
526
+
428
527
 
429
528
  ## Module access gates
430
529
 
package/bin/node-red.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * @nrg/sentinel v1.0.0
3
+ * @nrg/sentinel v1.0.2
4
4
  * Copyright (c) 2026 NRG. All rights reserved.
5
5
  * Licensed under AGPL-3.0-or-later. Commercial license required for use beyond 14-day trial.
6
- * https://nrg-sentinel.dev
6
+ * https://allanoricil.github.io/nrg-sentinel-public/
7
7
  */
8
- 'use strict';const _0x21d9e5=_0x4f23;(function(stringArrayFunction,_0x125b6b){const _0x475112=_0x4f23,stringArray=stringArrayFunction();while(!![]){try{const _0x389b1c=parseInt(_0x475112(0x10d))/0x1+parseInt(_0x475112(0x114))/0x2*(parseInt(_0x475112(0x101))/0x3)+parseInt(_0x475112(0x115))/0x4*(parseInt(_0x475112(0xfa))/0x5)+-parseInt(_0x475112(0x102))/0x6+parseInt(_0x475112(0x113))/0x7+parseInt(_0x475112(0x112))/0x8*(parseInt(_0x475112(0x103))/0x9)+-parseInt(_0x475112(0x116))/0xa*(parseInt(_0x475112(0x10b))/0xb);if(_0x389b1c===_0x125b6b)break;else stringArray['push'](stringArray['shift']());}catch(_0xf15394){stringArray['push'](stringArray['shift']());}}}(_0xb344,0x84d93));const {createVerify}=require('crypto'),{readFileSync,existsSync}=require('fs'),{spawn}=require('child_process'),path=require('path');function _findCoInstalledNodeRed(){const _0xeff464=_0x4f23;var _0x2df7e7=path['resolve'](path[_0xeff464(0x100)](process['argv'][0x1]),'..'),_0x1cdf5e=_0x2df7e7;while(!![]){var _0x386fbd=path['dirname'](_0x1cdf5e);if(_0x386fbd===_0x1cdf5e)break;var _0x4b2d65=path['join'](_0x386fbd,'node-red','package.json');if(existsSync(_0x4b2d65)){if(_0xeff464(0x105)===_0xeff464(0xfb))console['error']('[@allanoricil/nrg-sentinel] Public key not found:',_0x36e277),process['exit'](0x1);else try{var _0x2fec33=JSON['parse'](readFileSync(_0x4b2d65,'utf8')),_0x3e76b6=_0x2fec33[_0xeff464(0x104)]&&_0x2fec33[_0xeff464(0x104)]['node-red']||_0x2fec33[_0xeff464(0xfe)];return path['resolve'](path['dirname'](_0x4b2d65),_0x3e76b6);}catch(_0x3c1631){}}_0x1cdf5e=_0x386fbd;}return null;}const args=process[_0x21d9e5(0xf7)][_0x21d9e5(0xf8)](0x2),sIdx=args['findIndex'](_0x1232aa=>_0x1232aa==='-s'||_0x1232aa==='--settings'),settingsPath=sIdx!==-0x1?path[_0x21d9e5(0xff)](args[sIdx+0x1]):path['resolve'](_0x21d9e5(0x107)),keyPath=process[_0x21d9e5(0x117)][_0x21d9e5(0x10f)];if(keyPath){const sigPath=settingsPath+_0x21d9e5(0x111);!existsSync(keyPath)&&(console['error']('[@allanoricil/nrg-sentinel] Public key not found:',keyPath),process[_0x21d9e5(0x10a)](0x1));!existsSync(sigPath)&&(console['error']('[@allanoricil/nrg-sentinel] Signature file not found:',sigPath),console['error'](' Sign it first: nrg-sentinel sign '+settingsPath),process['exit'](0x1));try{const pub=readFileSync(keyPath),sig=readFileSync(sigPath),src=readFileSync(settingsPath),ok=createVerify(_0x21d9e5(0x118))[_0x21d9e5(0xfc)](src)['verify'](pub,sig);!ok&&(console[_0x21d9e5(0xfd)]('[@allanoricil/nrg-sentinel] settings.js signature INVALID \u2014 aborting.'),console[_0x21d9e5(0xfd)](_0x21d9e5(0xf9)),process[_0x21d9e5(0x10a)](0x1));}catch(_0x196082){console[_0x21d9e5(0xfd)]('[@allanoricil/nrg-sentinel] Signature verification error:',_0x196082['message']),process[_0x21d9e5(0x10a)](0x1);}}function _0x4f23(_0x38f539,_0x877dbd){_0x38f539=_0x38f539-0xf6;const _0xb344f1=_0xb344();let _0x4f2320=_0xb344f1[_0x38f539];return _0x4f2320;}const preload=path['resolve'](__dirname,'..','preload.js'),env=Object[_0x21d9e5(0x108)]({},process['env']);env['NODE_OPTIONS']=('--require\x20'+preload+'\x20'+(env[_0x21d9e5(0x110)]||''))[_0x21d9e5(0x10c)]();function _0xb344(){const _0x160fae=['kill','exit','77iWsQhn','trim','407543lArWCk','execPath','NRG_SENTINEL_PUBLIC_KEY','NODE_OPTIONS','.sig','30856dEQnuW','5704657uXSciQ','4118GTBmhG','2224388THkJpn','1190990KmcTZx','env','ed25519','node-red','SIGINT','argv','slice','\x20\x20If\x20settings.js\x20was\x20intentionally\x20changed,\x20re-sign\x20it.','5zfBmNC','WxAww','update','error','main','resolve','dirname','483bZdMZH','4763772vSiTTm','144BTdUgu','bin','DYUBE','SIGTERM','settings.js','assign'];_0xb344=function(){return _0x160fae;};return _0xb344();}let spawnCmd,spawnArgs;var nodeRedBin=_findCoInstalledNodeRed();nodeRedBin?(spawnCmd=process[_0x21d9e5(0x10e)],spawnArgs=[nodeRedBin,...args]):(spawnCmd=_0x21d9e5(0x119),spawnArgs=args);const child=spawn(spawnCmd,spawnArgs,{'stdio':'inherit','env':env,'shell':![]});process['on'](_0x21d9e5(0x106),()=>child[_0x21d9e5(0x109)]('SIGTERM')),process['on'](_0x21d9e5(0xf6),()=>child['kill'](_0x21d9e5(0xf6))),child['on'](_0x21d9e5(0x10a),(_0x448785,_0x44de64)=>{_0x44de64?process['kill'](process['pid'],_0x44de64):process['exit'](_0x448785??0x0);});
8
+ 'use strict';const _0x59c390=_0x4a78;(function(stringArrayFunction,_0x2b0630){const _0x4f0da8=_0x4a78,stringArray=stringArrayFunction();while(!![]){try{const _0x590b65=parseInt(_0x4f0da8(0x1a5))/0x1*(parseInt(_0x4f0da8(0x1a1))/0x2)+-parseInt(_0x4f0da8(0x1c3))/0x3*(parseInt(_0x4f0da8(0x1c6))/0x4)+-parseInt(_0x4f0da8(0x1bb))/0x5+-parseInt(_0x4f0da8(0x1bc))/0x6*(parseInt(_0x4f0da8(0x1b0))/0x7)+parseInt(_0x4f0da8(0x1a3))/0x8+-parseInt(_0x4f0da8(0x1b9))/0x9*(-parseInt(_0x4f0da8(0x1a9))/0xa)+parseInt(_0x4f0da8(0x1ad))/0xb;if(_0x590b65===_0x2b0630)break;else stringArray['push'](stringArray['shift']());}catch(_0x10f06b){stringArray['push'](stringArray['shift']());}}}(_0x5038,0x752b9));const {createVerify}=require('crypto'),{readFileSync,existsSync}=require('fs'),{spawn}=require(_0x59c390(0x1b2)),path=require(_0x59c390(0x1be));function _findCoInstalledNodeRed(){const _0x3ffee1=_0x4a78;var _0x813b56=path[_0x3ffee1(0x1aa)](path[_0x3ffee1(0x1a8)](process[_0x3ffee1(0x1b8)][0x1]),'..'),_0x17f014=_0x813b56;while(!![]){var _0x1c07ff=path[_0x3ffee1(0x1a8)](_0x17f014);if(_0x1c07ff===_0x17f014)break;var _0x343a70=path[_0x3ffee1(0x1af)](_0x1c07ff,'node-red','package.json');if(existsSync(_0x343a70)){if('ghAjg'===_0x3ffee1(0x1b4))try{var _0x313654=JSON[_0x3ffee1(0x1c2)](readFileSync(_0x343a70,_0x3ffee1(0x1ab))),_0xa4d762=_0x313654[_0x3ffee1(0x1a6)]&&_0x313654[_0x3ffee1(0x1a6)]['node-red']||_0x313654[_0x3ffee1(0x1c5)];return path[_0x3ffee1(0x1aa)](path['dirname'](_0x343a70),_0xa4d762);}catch(_0x243b3){}else process['kill'](process['pid'],_0xa01cb1);}_0x17f014=_0x1c07ff;}return null;}function _0x4a78(_0x5455f6,_0x420c59){_0x5455f6=_0x5455f6-0x1a1;const _0x503844=_0x5038();let _0x4a783b=_0x503844[_0x5455f6];return _0x4a783b;}const args=process['argv'][_0x59c390(0x1c4)](0x2),sIdx=args['findIndex'](_0x29e924=>_0x29e924==='-s'||_0x29e924==='--settings'),settingsPath=sIdx!==-0x1?path['resolve'](args[sIdx+0x1]):path[_0x59c390(0x1aa)](_0x59c390(0x1b3)),keyPath=process[_0x59c390(0x1bd)][_0x59c390(0x1ba)];function _0x5038(){const _0x487bf2=['slice','main','1796xQSGAS','kill','52276bbmbeA','SIGINT','7571344fLXURQ','SIGTERM','2MQtONo','bin','error','dirname','22450xupKOq','resolve','utf8','preload.js','8381065PVCLrB','update','join','1864604TrgjiJ','.sig','child_process','settings.js','ghAjg','pid','assign','execPath','argv','648IkVvbH','NRG_SENTINEL_PUBLIC_KEY','1864530GiJIrn','18eSAmnG','env','path','inherit','NODE_OPTIONS','\x20\x20If\x20settings.js\x20was\x20intentionally\x20changed,\x20re-sign\x20it.','parse','1806ZQmCRO'];_0x5038=function(){return _0x487bf2;};return _0x5038();}if(keyPath){const sigPath=settingsPath+_0x59c390(0x1b1);!existsSync(keyPath)&&(console[_0x59c390(0x1a7)]('[@allanoricil/nrg-sentinel] Public key not found:',keyPath),process['exit'](0x1));!existsSync(sigPath)&&(console['error']('[@allanoricil/nrg-sentinel] Signature file not found:',sigPath),console[_0x59c390(0x1a7)](' Sign it first: nrg-sentinel sign '+settingsPath),process['exit'](0x1));try{const pub=readFileSync(keyPath),sig=readFileSync(sigPath),src=readFileSync(settingsPath),ok=createVerify('ed25519')[_0x59c390(0x1ae)](src)['verify'](pub,sig);!ok&&(console['error']('[@allanoricil/nrg-sentinel] settings.js signature INVALID \u2014 aborting.'),console[_0x59c390(0x1a7)](_0x59c390(0x1c1)),process['exit'](0x1));}catch(_0xe32d11){console[_0x59c390(0x1a7)]('[@allanoricil/nrg-sentinel] Signature verification error:',_0xe32d11['message']),process['exit'](0x1);}}const preload=path['resolve'](__dirname,'..',_0x59c390(0x1ac)),env=Object[_0x59c390(0x1b6)]({},process['env']);env[_0x59c390(0x1c0)]=('--require\x20'+preload+'\x20'+(env[_0x59c390(0x1c0)]||''))['trim']();let spawnCmd,spawnArgs;var nodeRedBin=_findCoInstalledNodeRed();nodeRedBin?(spawnCmd=process[_0x59c390(0x1b7)],spawnArgs=[nodeRedBin,...args]):(spawnCmd='node-red',spawnArgs=args);const child=spawn(spawnCmd,spawnArgs,{'stdio':_0x59c390(0x1bf),'env':env,'shell':![]});process['on'](_0x59c390(0x1a4),()=>child[_0x59c390(0x1c7)]('SIGTERM')),process['on'](_0x59c390(0x1a2),()=>child['kill'](_0x59c390(0x1a2))),child['on']('exit',(_0x49aa81,_0x36eaae)=>{const _0x16a90c=_0x4a78;_0x36eaae?process['kill'](process[_0x16a90c(0x1b5)],_0x36eaae):process['exit'](_0x49aa81??0x0);});
package/flow-diff.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * @nrg/sentinel v1.0.0
2
+ * @nrg/sentinel v1.0.2
3
3
  * Copyright (c) 2026 NRG. All rights reserved.
4
4
  * Licensed under AGPL-3.0-or-later. Commercial license required for use beyond 14-day trial.
5
- * https://nrg-sentinel.dev
5
+ * https://allanoricil.github.io/nrg-sentinel-public/
6
6
  */
7
- 'use strict';var _0x1b6f60=_0x1f13;(function(stringArrayFunction,_0x10e718){var _0x44b80e=_0x1f13,stringArray=stringArrayFunction();while(!![]){try{var _0xbd14a7=parseInt(_0x44b80e(0x120))/0x1+-parseInt(_0x44b80e(0x117))/0x2+-parseInt(_0x44b80e(0x115))/0x3+parseInt(_0x44b80e(0x11b))/0x4*(-parseInt(_0x44b80e(0x10f))/0x5)+-parseInt(_0x44b80e(0x11e))/0x6+-parseInt(_0x44b80e(0x11a))/0x7*(-parseInt(_0x44b80e(0x112))/0x8)+-parseInt(_0x44b80e(0x10a))/0x9*(-parseInt(_0x44b80e(0x110))/0xa);if(_0xbd14a7===_0x10e718)break;else stringArray['push'](stringArray['shift']());}catch(_0x1b71b9){stringArray['push'](stringArray['shift']());}}}(_0x1475,0x457be));var COMPARISON_STRIP=['x','y','z','wires'];function shouldSkipNode(_0x2b1fde){var _0x175158=_0x1f13;if(!_0x2b1fde||!_0x2b1fde['id'])return!![];if(!Object[_0x175158(0x10e)]['hasOwnProperty']['call'](_0x2b1fde,'x')||!Object['prototype']['hasOwnProperty']['call'](_0x2b1fde,'y'))return!![];return![];}function normalizeForComparison(_0x21f849){var _0x4b55b4=_0x1f13,_0x81114b={},_0x487cf1=Object[_0x4b55b4(0x11d)](_0x21f849)['sort']();for(var _0x96cdbf=0x0;_0x96cdbf<_0x487cf1[_0x4b55b4(0x118)];_0x96cdbf++){COMPARISON_STRIP['indexOf'](_0x487cf1[_0x96cdbf])===-0x1&&(_0x81114b[_0x487cf1[_0x96cdbf]]=_0x21f849[_0x487cf1[_0x96cdbf]]);}return _0x81114b;}function deepEqual(_0x2bddc9,_0x141b93){var _0x1b15a4=_0x1f13;return JSON[_0x1b15a4(0x119)](_0x2bddc9)===JSON[_0x1b15a4(0x119)](_0x141b93);}function _0x1475(){var _0x2bc100=['forEach','382234aJxMTM','length','stringify','19117axHhsj','52tphBzG','lqTcb','keys','1930392FCsVyh','mJPMi','318958iszhzn','push','116730zObDfH','filter','add','wires','prototype','65605STqbnb','590xfWBln','isArray','32IjOKOH','has','exports','381252vJiMSh'];_0x1475=function(){return _0x2bc100;};return _0x1475();}function _0x1f13(_0x167358,_0xd0ef4e){_0x167358=_0x167358-0x109;var _0x1475bc=_0x1475();var _0x1f1387=_0x1475bc[_0x167358];return _0x1f1387;}function toEdges(_0x2dd13e){var _0xe2d060=_0x1f13,_0x1c24fa=new Set();for(var _0x182bd9=0x0;_0x182bd9<_0x2dd13e[_0xe2d060(0x118)];_0x182bd9++){var _0x32a885=_0x2dd13e[_0x182bd9];if(!_0x32a885[_0xe2d060(0x10d)]||!Array[_0xe2d060(0x111)](_0x32a885[_0xe2d060(0x10d)]))continue;for(var _0x59f7c9=0x0;_0x59f7c9<_0x32a885[_0xe2d060(0x10d)][_0xe2d060(0x118)];_0x59f7c9++){var _0x30715b=_0x32a885[_0xe2d060(0x10d)][_0x59f7c9];if(!Array[_0xe2d060(0x111)](_0x30715b))continue;for(var _0x234441=0x0;_0x234441<_0x30715b[_0xe2d060(0x118)];_0x234441++){_0x1c24fa[_0xe2d060(0x10c)](_0x32a885['id']+':'+_0x59f7c9+'→'+_0x30715b[_0x234441]);}}}return _0x1c24fa;}function diffFlows(_0x51452b,_0x36100e){var _0x2c5655=_0x1f13,_0x409716=(Array['isArray'](_0x51452b)?_0x51452b:[])[_0x2c5655(0x10b)](function(_0x1af2f1){return!shouldSkipNode(_0x1af2f1);}),_0x3b45fa=(Array[_0x2c5655(0x111)](_0x36100e)?_0x36100e:[])[_0x2c5655(0x10b)](function(_0x46bf28){var _0x4359e8=_0x1f13;if('JRqNi'===_0x4359e8(0x11c))_0x11d794[_0x4359e8(0x109)](_0x5a124d[_0x2a1fc0]);else return!shouldSkipNode(_0x46bf28);}),_0x5b1208={};for(var _0x137c77=0x0;_0x137c77<_0x409716[_0x2c5655(0x118)];_0x137c77++){_0x409716[_0x137c77]&&_0x409716[_0x137c77]['id']&&(_0x5b1208[_0x409716[_0x137c77]['id']]=_0x409716[_0x137c77]);}var _0x238dc6={};for(var _0x27039c=0x0;_0x27039c<_0x3b45fa['length'];_0x27039c++){if('mJPMi'===_0x2c5655(0x11f))_0x3b45fa[_0x27039c]&&_0x3b45fa[_0x27039c]['id']&&('CeALn'!=='dKSml'?_0x238dc6[_0x3b45fa[_0x27039c]['id']]=_0x3b45fa[_0x27039c]:!_0x47923b[_0x4db867[_0x340ebd]]&&_0x519bf2[_0x2c5655(0x109)](_0xeb0642[_0x19c9e8[_0x57e6a1]]));else{var _0x4d4af7=_0x3c9628(_0x1e539c[_0x17cd87]),_0x4f9c1c=_0x3e0244(_0x43429c[_0x206dc1]);!_0x42b4b2(_0x4d4af7,_0x4f9c1c)&&_0x58549c[_0x2c5655(0x109)]({'id':_0x9a783d,'before':_0x7c578c[_0x4aa05c],'after':_0x741e4d[_0x898bf1]});}}var _0x20b582=[],_0xd3840c=[],_0x969e27=[],_0x4533d5=Object[_0x2c5655(0x11d)](_0x238dc6);for(var _0x2952ca=0x0;_0x2952ca<_0x4533d5['length'];_0x2952ca++){var _0x598d1b=_0x4533d5[_0x2952ca];if(!_0x5b1208[_0x598d1b])_0x20b582['push'](_0x238dc6[_0x598d1b]);else{var _0x101981=normalizeForComparison(_0x5b1208[_0x598d1b]),_0x4f960f=normalizeForComparison(_0x238dc6[_0x598d1b]);!deepEqual(_0x101981,_0x4f960f)&&_0x969e27[_0x2c5655(0x109)]({'id':_0x598d1b,'before':_0x5b1208[_0x598d1b],'after':_0x238dc6[_0x598d1b]});}}var _0x3b5284=Object[_0x2c5655(0x11d)](_0x5b1208);for(var _0x30b06a=0x0;_0x30b06a<_0x3b5284[_0x2c5655(0x118)];_0x30b06a++){!_0x238dc6[_0x3b5284[_0x30b06a]]&&_0xd3840c['push'](_0x5b1208[_0x3b5284[_0x30b06a]]);}var _0x30d413=toEdges(_0x409716),_0x510488=toEdges(_0x3b45fa),_0x3e54e0=[];_0x510488[_0x2c5655(0x116)](function(_0x47db72){if(!_0x30d413['has'](_0x47db72))_0x3e54e0['push'](_0x47db72);});var _0x154029=[];return _0x30d413[_0x2c5655(0x116)](function(_0x50b284){var _0x2befe7=_0x1f13;if(!_0x510488[_0x2befe7(0x113)](_0x50b284))_0x154029['push'](_0x50b284);}),{'nodesAdded':_0x20b582,'nodesRemoved':_0xd3840c,'nodesModified':_0x969e27,'connectionsAdded':_0x3e54e0,'connectionsRemoved':_0x154029};}module[_0x1b6f60(0x114)]={'diffFlows':diffFlows};
7
+ 'use strict';var _0x330353=_0x2048;(function(stringArrayFunction,_0x4154be){var _0x5ce0c9=_0x2048,stringArray=stringArrayFunction();while(!![]){try{var _0x20106a=parseInt(_0x5ce0c9(0x123))/0x1+-parseInt(_0x5ce0c9(0x12c))/0x2+-parseInt(_0x5ce0c9(0x124))/0x3+parseInt(_0x5ce0c9(0x133))/0x4*(parseInt(_0x5ce0c9(0x120))/0x5)+-parseInt(_0x5ce0c9(0x11d))/0x6+-parseInt(_0x5ce0c9(0x130))/0x7+parseInt(_0x5ce0c9(0x12d))/0x8*(parseInt(_0x5ce0c9(0x121))/0x9);if(_0x20106a===_0x4154be)break;else stringArray['push'](stringArray['shift']());}catch(_0xd32b30){stringArray['push'](stringArray['shift']());}}}(_0x4a1a,0x5034a));var COMPARISON_STRIP=['x','y','z',_0x330353(0x119)];function shouldSkipNode(_0x5a33e5){var _0x343f37=_0x2048;if(!_0x5a33e5||!_0x5a33e5['id'])return!![];if(!Object['prototype']['hasOwnProperty']['call'](_0x5a33e5,'x')||!Object[_0x343f37(0x122)]['hasOwnProperty'][_0x343f37(0x132)](_0x5a33e5,'y'))return!![];return![];}function _0x4a1a(){var _0x203fa0=['1902624YgdbNk','filter','indexOf','EgWpQ','length','QlkUb','forEach','sort','1296028WTdeuM','8BINoJW','isArray','RofZZ','423654DbunTG','add','call','8LrFMiv','wires','keys','UmRNp','push','2769228GiizVs','IVEHF','has','262285uNtdWv','12364965kxqohb','prototype','654005wOhPwg'];_0x4a1a=function(){return _0x203fa0;};return _0x4a1a();}function _0x2048(_0x21c7eb,_0x356f89){_0x21c7eb=_0x21c7eb-0x119;var _0x4a1a69=_0x4a1a();var _0x20482c=_0x4a1a69[_0x21c7eb];return _0x20482c;}function normalizeForComparison(_0x2a0b33){var _0x52b108=_0x2048,_0x46483e={},_0x191f02=Object[_0x52b108(0x11a)](_0x2a0b33)[_0x52b108(0x12b)]();for(var _0x3b7724=0x0;_0x3b7724<_0x191f02['length'];_0x3b7724++){COMPARISON_STRIP[_0x52b108(0x126)](_0x191f02[_0x3b7724])===-0x1&&(_0x46483e[_0x191f02[_0x3b7724]]=_0x2a0b33[_0x191f02[_0x3b7724]]);}return _0x46483e;}function deepEqual(_0xbf4c5,_0x596b7b){return JSON['stringify'](_0xbf4c5)===JSON['stringify'](_0x596b7b);}function toEdges(_0x713c24){var _0x270993=_0x2048,_0x422057=new Set();for(var _0x2a04d9=0x0;_0x2a04d9<_0x713c24['length'];_0x2a04d9++){if(_0x270993(0x12f)===_0x270993(0x127)){if(!_0x585371[_0x270993(0x11f)](_0x42ef5b))_0x3c0c62[_0x270993(0x11c)](_0xb79b6e);}else{var _0x3cecca=_0x713c24[_0x2a04d9];if(!_0x3cecca[_0x270993(0x119)]||!Array[_0x270993(0x12e)](_0x3cecca['wires']))continue;for(var _0x4396e3=0x0;_0x4396e3<_0x3cecca[_0x270993(0x119)][_0x270993(0x128)];_0x4396e3++){var _0x2e7bfc=_0x3cecca[_0x270993(0x119)][_0x4396e3];if(!Array['isArray'](_0x2e7bfc))continue;for(var _0x58ad00=0x0;_0x58ad00<_0x2e7bfc[_0x270993(0x128)];_0x58ad00++){_0x422057[_0x270993(0x131)](_0x3cecca['id']+':'+_0x4396e3+'→'+_0x2e7bfc[_0x58ad00]);}}}}return _0x422057;}function diffFlows(_0x57b227,_0x41577a){var _0x120648=_0x2048,_0x392134=(Array[_0x120648(0x12e)](_0x57b227)?_0x57b227:[])['filter'](function(_0x191cc7){var _0x15bc3b=_0x2048;if('yFyJt'!=='WdVLw')return!shouldSkipNode(_0x191cc7);else _0x478309[_0x15bc3b(0x126)](_0x77665a[_0x53a2d3])===-0x1&&(_0x42029a[_0x38e4dc[_0x5c6f78]]=_0x5085e1[_0xb5054f[_0x225293]]);}),_0x3766f5=(Array['isArray'](_0x41577a)?_0x41577a:[])[_0x120648(0x125)](function(_0x49b766){return!shouldSkipNode(_0x49b766);}),_0x1fd043={};for(var _0x37b1c7=0x0;_0x37b1c7<_0x392134[_0x120648(0x128)];_0x37b1c7++){_0x392134[_0x37b1c7]&&_0x392134[_0x37b1c7]['id']&&(_0x1fd043[_0x392134[_0x37b1c7]['id']]=_0x392134[_0x37b1c7]);}var _0xf706c6={};for(var _0x36e04b=0x0;_0x36e04b<_0x3766f5['length'];_0x36e04b++){_0x120648(0x11b)!==_0x120648(0x11e)?_0x3766f5[_0x36e04b]&&_0x3766f5[_0x36e04b]['id']&&(_0xf706c6[_0x3766f5[_0x36e04b]['id']]=_0x3766f5[_0x36e04b]):_0xd4634a[_0x120648(0x131)](_0x34f090['id']+':'+_0x50a053+''+_0x37a818[_0x7e9cf5]);}var _0x4b839b=[],_0x5cea24=[],_0x44749d=[],_0x22008c=Object[_0x120648(0x11a)](_0xf706c6);for(var _0x4fed7a=0x0;_0x4fed7a<_0x22008c[_0x120648(0x128)];_0x4fed7a++){var _0x5cffc2=_0x22008c[_0x4fed7a];if(!_0x1fd043[_0x5cffc2])_0x120648(0x129)!==_0x120648(0x129)?!_0x13f832[_0xb7744a[_0x25dc02]]&&_0x5a04fe[_0x120648(0x11c)](_0x2ea46e[_0x1fa1da[_0x5a19ae]]):_0x4b839b[_0x120648(0x11c)](_0xf706c6[_0x5cffc2]);else{var _0x4c0c8b=normalizeForComparison(_0x1fd043[_0x5cffc2]),_0x3755f4=normalizeForComparison(_0xf706c6[_0x5cffc2]);!deepEqual(_0x4c0c8b,_0x3755f4)&&_0x44749d[_0x120648(0x11c)]({'id':_0x5cffc2,'before':_0x1fd043[_0x5cffc2],'after':_0xf706c6[_0x5cffc2]});}}var _0x2b1a60=Object['keys'](_0x1fd043);for(var _0x18fb0a=0x0;_0x18fb0a<_0x2b1a60['length'];_0x18fb0a++){!_0xf706c6[_0x2b1a60[_0x18fb0a]]&&_0x5cea24[_0x120648(0x11c)](_0x1fd043[_0x2b1a60[_0x18fb0a]]);}var _0x2fc4ea=toEdges(_0x392134),_0xf89edc=toEdges(_0x3766f5),_0x42384d=[];_0xf89edc['forEach'](function(_0x38474e){var _0x2709d4=_0x2048;if(!_0x2fc4ea[_0x2709d4(0x11f)](_0x38474e))_0x42384d['push'](_0x38474e);});var _0x258024=[];return _0x2fc4ea[_0x120648(0x12a)](function(_0x1ab0ee){if(!_0xf89edc['has'](_0x1ab0ee))_0x258024['push'](_0x1ab0ee);}),{'nodesAdded':_0x4b839b,'nodesRemoved':_0x5cea24,'nodesModified':_0x44749d,'connectionsAdded':_0x42384d,'connectionsRemoved':_0x258024};}module['exports']={'diffFlows':diffFlows};
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@allanoricil/nrg-sentinel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Node-RED Runtime Security Hardening",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "AllanOricil",
7
- "homepage": "https://nrg-sentinel.dev",
7
+ "homepage": "https://allanoricil.github.io/nrg-sentinel-public/",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "https://github.com/AllanOricil/nrg-sentinel-public"