@allanoricil/nrg-sentinel 1.0.0 → 1.0.1
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:18:54Z_
|
|
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.1
|
|
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 _0xc4287=_0x4cdd;(function(stringArrayFunction,_0x264e5f){const _0x37a459=_0x4cdd,stringArray=stringArrayFunction();while(!![]){try{const _0xe8e9fd=-parseInt(_0x37a459(0x8b))/0x1*(parseInt(_0x37a459(0x9f))/0x2)+parseInt(_0x37a459(0x99))/0x3+parseInt(_0x37a459(0x82))/0x4*(-parseInt(_0x37a459(0x81))/0x5)+parseInt(_0x37a459(0xa0))/0x6*(-parseInt(_0x37a459(0x84))/0x7)+parseInt(_0x37a459(0x94))/0x8+-parseInt(_0x37a459(0x83))/0x9+parseInt(_0x37a459(0x8a))/0xa;if(_0xe8e9fd===_0x264e5f)break;else stringArray['push'](stringArray['shift']());}catch(_0x20995a){stringArray['push'](stringArray['shift']());}}}(_0x5889,0x9bf98));const {createVerify}=require('crypto'),{readFileSync,existsSync}=require('fs'),{spawn}=require(_0xc4287(0x95)),path=require('path');function _findCoInstalledNodeRed(){const _0x33838a=_0x4cdd;var _0x42c0bd=path[_0x33838a(0xa3)](path[_0x33838a(0x9c)](process[_0x33838a(0x9b)][0x1]),'..'),_0x54d9f2=_0x42c0bd;while(!![]){var _0x4d0577=path[_0x33838a(0x9c)](_0x54d9f2);if(_0x4d0577===_0x54d9f2)break;var _0x501100=path[_0x33838a(0xa5)](_0x4d0577,_0x33838a(0x8f),_0x33838a(0x9d));if(existsSync(_0x501100)){if('ZxzXR'==='HPOOu')process[_0x33838a(0x92)](process['pid'],_0x41bce5);else try{if('aTOBe'===_0x33838a(0x96)){var _0x29cfb2=JSON[_0x33838a(0xa4)](readFileSync(_0x501100,_0x33838a(0x8d))),_0x4373ba=_0x29cfb2['bin']&&_0x29cfb2[_0x33838a(0x8e)]['node-red']||_0x29cfb2['main'];return path['resolve'](path[_0x33838a(0x9c)](_0x501100),_0x4373ba);}else{const _0x2dc207=_0x5eb7b4(_0x538108),_0x1eee13=_0x5e4a3a(_0x19282d),_0x2020cd=_0x756f91(_0x281914),_0xd4bcff=_0x2b276b(_0x33838a(0xa7))['update'](_0x2020cd)['verify'](_0x2dc207,_0x1eee13);!_0xd4bcff&&(console[_0x33838a(0xa2)]('[@allanoricil/nrg-sentinel] settings.js signature INVALID \u2014 aborting.'),console['error'](_0x33838a(0x89)),process['exit'](0x1));}}catch(_0x2d56df){}}_0x54d9f2=_0x4d0577;}return null;}const args=process[_0xc4287(0x9b)]['slice'](0x2),sIdx=args[_0xc4287(0x93)](_0x35816c=>_0x35816c==='-s'||_0x35816c===_0xc4287(0xa1)),settingsPath=sIdx!==-0x1?path[_0xc4287(0xa3)](args[sIdx+0x1]):path['resolve'](_0xc4287(0x85)),keyPath=process[_0xc4287(0x8c)]['NRG_SENTINEL_PUBLIC_KEY'];if(keyPath){const sigPath=settingsPath+'.sig';!existsSync(keyPath)&&(console[_0xc4287(0xa2)]('[@allanoricil/nrg-sentinel] Public key not found:',keyPath),process['exit'](0x1));!existsSync(sigPath)&&(console[_0xc4287(0xa2)]('[@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(_0xc4287(0xa7))[_0xc4287(0x88)](src)['verify'](pub,sig);!ok&&(console[_0xc4287(0xa2)]('[@allanoricil/nrg-sentinel] settings.js signature INVALID \u2014 aborting.'),console['error']('\x20\x20If\x20settings.js\x20was\x20intentionally\x20changed,\x20re-sign\x20it.'),process[_0xc4287(0x91)](0x1));}catch(_0x102545){console['error']('[@allanoricil/nrg-sentinel] Signature verification error:',_0x102545[_0xc4287(0x9a)]),process[_0xc4287(0x91)](0x1);}}function _0x5889(){const _0x298a0a=['bin','node-red','pid','exit','kill','findIndex','5477720YFqupO','child_process','aTOBe','NODE_OPTIONS','execPath','2355027QdzhVu','message','argv','dirname','package.json','trim','717202DUjycP','6WyjeRJ','--settings','error','resolve','parse','join','SIGINT','ed25519','5oQtEuB','2010076QxGAXi','352575EKsXed','7748055DwgwhI','settings.js','--require\x20','inherit','update','\x20\x20If\x20settings.js\x20was\x20intentionally\x20changed,\x20re-sign\x20it.','11763080ogGqMT','1ZDPutC','env','utf8'];_0x5889=function(){return _0x298a0a;};return _0x5889();}const preload=path['resolve'](__dirname,'..','preload.js'),env=Object['assign']({},process['env']);env['NODE_OPTIONS']=(_0xc4287(0x86)+preload+'\x20'+(env[_0xc4287(0x97)]||''))[_0xc4287(0x9e)]();let spawnCmd,spawnArgs;function _0x4cdd(_0x339101,_0x3bce3a){_0x339101=_0x339101-0x81;const _0x5889c5=_0x5889();let _0x4cdd96=_0x5889c5[_0x339101];return _0x4cdd96;}var nodeRedBin=_findCoInstalledNodeRed();nodeRedBin?(spawnCmd=process[_0xc4287(0x98)],spawnArgs=[nodeRedBin,...args]):(spawnCmd=_0xc4287(0x8f),spawnArgs=args);const child=spawn(spawnCmd,spawnArgs,{'stdio':_0xc4287(0x87),'env':env,'shell':![]});process['on']('SIGTERM',()=>child['kill']('SIGTERM')),process['on'](_0xc4287(0xa6),()=>child[_0xc4287(0x92)](_0xc4287(0xa6))),child['on'](_0xc4287(0x91),(_0x4473d7,_0x3d41e1)=>{const _0x3248ca=_0x4cdd;_0x3d41e1?process['kill'](process[_0x3248ca(0x90)],_0x3d41e1):'hFnMV'==='hFnMV'?process[_0x3248ca(0x91)](_0x4473d7??0x0):(_0x10deb2=process['execPath'],_0x16834a=[_0x120453,..._0x4bca51]);});
|
package/flow-diff.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @nrg/sentinel v1.0.
|
|
2
|
+
* @nrg/sentinel v1.0.1
|
|
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 _0x52f26c=_0x356f;(function(stringArrayFunction,_0x1c922a){var _0x747310=_0x356f,stringArray=stringArrayFunction();while(!![]){try{var _0x4b1a68=-parseInt(_0x747310(0x1f9))/0x1+parseInt(_0x747310(0x1ed))/0x2*(parseInt(_0x747310(0x1e3))/0x3)+-parseInt(_0x747310(0x1e6))/0x4*(parseInt(_0x747310(0x1f4))/0x5)+-parseInt(_0x747310(0x1ea))/0x6+-parseInt(_0x747310(0x1f2))/0x7+-parseInt(_0x747310(0x1f7))/0x8*(-parseInt(_0x747310(0x1fa))/0x9)+parseInt(_0x747310(0x1e5))/0xa;if(_0x4b1a68===_0x1c922a)break;else stringArray['push'](stringArray['shift']());}catch(_0x16176d){stringArray['push'](stringArray['shift']());}}}(_0x349c,0xc2e4e));function _0x356f(_0x11a72c,_0x239825){_0x11a72c=_0x11a72c-0x1e2;var _0x349c84=_0x349c();var _0x356fbd=_0x349c84[_0x11a72c];return _0x356fbd;}var COMPARISON_STRIP=['x','y','z',_0x52f26c(0x1e2)];function shouldSkipNode(_0x4f5dbc){var _0x17907c=_0x356f;if(!_0x4f5dbc||!_0x4f5dbc['id'])return!![];if(!Object[_0x17907c(0x1e4)]['hasOwnProperty'][_0x17907c(0x1f6)](_0x4f5dbc,'x')||!Object[_0x17907c(0x1e4)]['hasOwnProperty'][_0x17907c(0x1f6)](_0x4f5dbc,'y'))return!![];return![];}function normalizeForComparison(_0x2cd2ce){var _0xca6724=_0x356f,_0x2852dd={},_0x5521ed=Object['keys'](_0x2cd2ce)['sort']();for(var _0x4b8185=0x0;_0x4b8185<_0x5521ed[_0xca6724(0x1e7)];_0x4b8185++){'PBMQA'!==_0xca6724(0x1fc)?COMPARISON_STRIP[_0xca6724(0x1f8)](_0x5521ed[_0x4b8185])===-0x1&&('YQuJi'==='dLMnK'?_0x2da4ee[_0xca6724(0x1f8)](_0x41a237[_0x39d876])===-0x1&&(_0x24b593[_0x282a42[_0xe5b9e8]]=_0x1fae6d[_0x5cc5a8[_0x304f8e]]):_0x2852dd[_0x5521ed[_0x4b8185]]=_0x2cd2ce[_0x5521ed[_0x4b8185]]):!_0x3bb6a9[_0x822c49[_0x59c79f]]&&_0x1fd336['push'](_0x50fb97[_0x4fe9b0[_0x192973]]);}return _0x2852dd;}function deepEqual(_0x2032b0,_0x2fa340){return JSON['stringify'](_0x2032b0)===JSON['stringify'](_0x2fa340);}function toEdges(_0x39a7c0){var _0x59d6d8=_0x356f,_0x52d991=new Set();for(var _0x3184d6=0x0;_0x3184d6<_0x39a7c0['length'];_0x3184d6++){var _0x3b52d2=_0x39a7c0[_0x3184d6];if(!_0x3b52d2[_0x59d6d8(0x1e2)]||!Array['isArray'](_0x3b52d2[_0x59d6d8(0x1e2)]))continue;for(var _0x65bef2=0x0;_0x65bef2<_0x3b52d2['wires']['length'];_0x65bef2++){var _0x3d1cb0=_0x3b52d2[_0x59d6d8(0x1e2)][_0x65bef2];if(!Array['isArray'](_0x3d1cb0))continue;for(var _0xc884f4=0x0;_0xc884f4<_0x3d1cb0['length'];_0xc884f4++){_0x52d991[_0x59d6d8(0x1ee)](_0x3b52d2['id']+':'+_0x65bef2+'→'+_0x3d1cb0[_0xc884f4]);}}}return _0x52d991;}function diffFlows(_0x25bd6f,_0x476b8f){var _0x1a0b57=_0x356f,_0x2708f4=(Array[_0x1a0b57(0x1f3)](_0x25bd6f)?_0x25bd6f:[])['filter'](function(_0x48a706){return!shouldSkipNode(_0x48a706);}),_0x3229ad=(Array[_0x1a0b57(0x1f3)](_0x476b8f)?_0x476b8f:[])[_0x1a0b57(0x1f1)](function(_0x1d7b7f){var _0x380528=_0x356f;if(_0x380528(0x1ef)==='qdfUg')return!shouldSkipNode(_0x1d7b7f);else{var _0x569eda=_0x1f1bf6[_0x78963];if(!_0x461866[_0x569eda])_0x482d82[_0x380528(0x1e9)](_0x10a67d[_0x569eda]);else{var _0xb1858b=_0x463529(_0x35f1a5[_0x569eda]),_0x53cb59=_0x41bb9d(_0x227db1[_0x569eda]);!_0x63243d(_0xb1858b,_0x53cb59)&&_0x1c71bc[_0x380528(0x1e9)]({'id':_0x569eda,'before':_0x5861af[_0x569eda],'after':_0x11c15b[_0x569eda]});}}}),_0x18c571={};for(var _0x2c7630=0x0;_0x2c7630<_0x2708f4[_0x1a0b57(0x1e7)];_0x2c7630++){_0x1a0b57(0x1eb)==='NHnXa'?_0x2708f4[_0x2c7630]&&_0x2708f4[_0x2c7630]['id']&&(_0x18c571[_0x2708f4[_0x2c7630]['id']]=_0x2708f4[_0x2c7630]):_0x4891c0[_0x1a0b57(0x1e9)]({'id':_0x11f49e,'before':_0x15aad7[_0x37e26f],'after':_0x148fd0[_0x2f8fe3]});}var _0x18f162={};for(var _0x49c2d2=0x0;_0x49c2d2<_0x3229ad[_0x1a0b57(0x1e7)];_0x49c2d2++){if(_0x3229ad[_0x49c2d2]&&_0x3229ad[_0x49c2d2]['id']){if('jzrGQ'!==_0x1a0b57(0x1e8))_0x18f162[_0x3229ad[_0x49c2d2]['id']]=_0x3229ad[_0x49c2d2];else{var _0xf5502f=_0x558ca0(_0x97bd41[_0x3f6479]),_0x579daa=_0x2957d7(_0x5c299c[_0x4edb19]);!_0x1e410c(_0xf5502f,_0x579daa)&&_0x1c6d1b[_0x1a0b57(0x1e9)]({'id':_0x4c40e8,'before':_0x3525b3[_0x5ae195],'after':_0x854982[_0x277341]});}}}var _0x5efac1=[],_0x16129d=[],_0x14d709=[],_0x498fd6=Object['keys'](_0x18f162);for(var _0x3dbf39=0x0;_0x3dbf39<_0x498fd6['length'];_0x3dbf39++){var _0x12a7ff=_0x498fd6[_0x3dbf39];if(!_0x18c571[_0x12a7ff])_0x5efac1['push'](_0x18f162[_0x12a7ff]);else{var _0x1b55c1=normalizeForComparison(_0x18c571[_0x12a7ff]),_0x518358=normalizeForComparison(_0x18f162[_0x12a7ff]);!deepEqual(_0x1b55c1,_0x518358)&&_0x14d709['push']({'id':_0x12a7ff,'before':_0x18c571[_0x12a7ff],'after':_0x18f162[_0x12a7ff]});}}var _0x348e0a=Object[_0x1a0b57(0x1ec)](_0x18c571);for(var _0x4535a9=0x0;_0x4535a9<_0x348e0a[_0x1a0b57(0x1e7)];_0x4535a9++){if(!_0x18f162[_0x348e0a[_0x4535a9]]){if('pxuMD'===_0x1a0b57(0x1f0))_0x16129d[_0x1a0b57(0x1e9)](_0x18c571[_0x348e0a[_0x4535a9]]);else return JSON['stringify'](_0xa44a94)===JSON['stringify'](_0x687221);}}var _0x16dcf3=toEdges(_0x2708f4),_0x42cbea=toEdges(_0x3229ad),_0x2d684a=[];_0x42cbea['forEach'](function(_0x171d0c){var _0x38bb5a=_0x356f;if(!_0x16dcf3[_0x38bb5a(0x1fb)](_0x171d0c))_0x2d684a[_0x38bb5a(0x1e9)](_0x171d0c);});var _0x2d4f6a=[];return _0x16dcf3['forEach'](function(_0x3e2cab){if(!_0x42cbea['has'](_0x3e2cab))_0x2d4f6a['push'](_0x3e2cab);}),{'nodesAdded':_0x5efac1,'nodesRemoved':_0x16129d,'nodesModified':_0x14d709,'connectionsAdded':_0x2d684a,'connectionsRemoved':_0x2d4f6a};}module[_0x52f26c(0x1f5)]={'diffFlows':diffFlows};function _0x349c(){var _0x51e54c=['has','SZxus','wires','198039EjNDiM','prototype','18141380UQWkoK','244eictOm','length','CIQVI','push','2923728tCqAdc','NHnXa','keys','14ByJbVU','add','qdfUg','pxuMD','filter','6420022IuUOGO','isArray','88610iZJSIR','exports','call','12192088LvanVf','indexOf','516478YujeSj','9TjKusO'];_0x349c=function(){return _0x51e54c;};return _0x349c();}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allanoricil/nrg-sentinel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
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"
|