@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 +128 -29
- package/bin/node-red.js +3 -3
- package/flow-diff.js +3 -3
- package/package.json +2 -2
- package/plugin.html +2 -2
- package/plugin.js +3 -3
- package/preload.js +3 -3
- package/review-ui.html +2 -2
- package/safe-deployment-queue.js +3 -3
- package/service-worker.js +3 -3
- package/sw-register.js +3 -3
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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
224
|
+
// Reads this.credentials on the raw this — not 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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
266
|
-
//
|
|
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
|
|
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"
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
-
"
|
|
298
|
-
"
|
|
336
|
+
"nodeTypes": {
|
|
337
|
+
"my-vault-config": {
|
|
338
|
+
"node:credentials:read": []
|
|
339
|
+
}
|
|
299
340
|
}
|
|
300
341
|
}
|
|
301
342
|
```
|
|
302
343
|
|
|
303
|
-
|
|
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
|
-
|
|
362
|
+
**Restrict wire rewiring to a specific tool package**
|
|
306
363
|
|
|
307
364
|
```json
|
|
308
365
|
{
|
|
309
|
-
"
|
|
310
|
-
"
|
|
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
|
-
|
|
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.
|
|
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
|
|
6
|
+
* https://allanoricil.github.io/nrg-sentinel-public/
|
|
7
7
|
*/
|
|
8
|
-
'use strict';const
|
|
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.
|
|
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
|
|
5
|
+
* https://allanoricil.github.io/nrg-sentinel-public/
|
|
6
6
|
*/
|
|
7
|
-
'use strict';var
|
|
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.
|
|
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
|
|
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"
|