n8n-nodes-redactor 1.11.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -10
- package/dist/credentials/RedactorVaultApi.credentials.js +23 -0
- package/dist/credentials/redactorVault.dark.svg +1 -0
- package/dist/credentials/redactorVault.svg +1 -0
- package/dist/nodes/Redactor/Redactor.node.js +177 -62
- package/dist/nodes/Redactor/audit.js +26 -99
- package/dist/nodes/Redactor/engine.js +199 -181
- package/dist/nodes/Redactor/injection.js +1 -1
- package/dist/nodes/Redactor/presidio.js +30 -84
- package/dist/nodes/Redactor/profiles.js +120 -101
- package/dist/nodes/Redactor/regexSafety.js +111 -0
- package/dist/nodes/Redactor/runtime.js +75 -0
- package/dist/nodes/Redactor/vault.js +188 -185
- package/package.json +14 -7
- package/dist/nodes/Redactor/Redactor.node.d.ts +0 -5
- package/dist/nodes/Redactor/audit.d.ts +0 -22
- package/dist/nodes/Redactor/classification.d.ts +0 -13
- package/dist/nodes/Redactor/context.d.ts +0 -7
- package/dist/nodes/Redactor/encryption.d.ts +0 -4
- package/dist/nodes/Redactor/engine.d.ts +0 -6
- package/dist/nodes/Redactor/injection.d.ts +0 -20
- package/dist/nodes/Redactor/names.d.ts +0 -8
- package/dist/nodes/Redactor/patterns.d.ts +0 -8
- package/dist/nodes/Redactor/presidio.d.ts +0 -27
- package/dist/nodes/Redactor/profiles.d.ts +0 -23
- package/dist/nodes/Redactor/pseudonymize.d.ts +0 -1
- package/dist/nodes/Redactor/ropa.d.ts +0 -51
- package/dist/nodes/Redactor/types.d.ts +0 -71
- package/dist/nodes/Redactor/vault.d.ts +0 -53
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
<a href="https://www.npmjs.com/package/n8n-nodes-redactor#provenance"><img src="https://img.shields.io/badge/npm-provenance%20signed-brightgreen" alt="npm provenance" /></a>
|
|
17
17
|
<a href="#license"><img src="https://img.shields.io/npm/l/n8n-nodes-redactor" alt="MIT License" /></a>
|
|
18
18
|
<img src="https://img.shields.io/badge/PII%20patterns-216-blue" alt="216 PII patterns" />
|
|
19
|
-
<img src="https://img.shields.io/badge/n8n-
|
|
19
|
+
<img src="https://img.shields.io/badge/n8n-Cloud%20%2B%20self--hosted-FF6D5A" alt="n8n Cloud and self-hosted" />
|
|
20
20
|
</p>
|
|
21
21
|
|
|
22
22
|
## What you get
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
- **The answer still makes sense.** After the model replies, the node puts the real values back, so your output reads naturally.
|
|
26
26
|
- **Fewer false alarms.** Card numbers are Luhn checked, IBANs checksum verified, NHS numbers mod 11 validated, so random digits are left alone.
|
|
27
27
|
- **Proof for your records.** An optional audit report, and a GDPR Article 30 processing record built from each run.
|
|
28
|
-
- **No new vendor.** Everything runs inside your
|
|
28
|
+
- **No new vendor.** Everything runs inside your n8n, on n8n Cloud or self-hosted. No extra service, no API key, no runtime dependency.
|
|
29
29
|
|
|
30
30
|
## From install to first result
|
|
31
31
|
|
|
@@ -50,11 +50,11 @@ After Restore, your workflow gets the real values back.
|
|
|
50
50
|
|
|
51
51
|
| Environment | Status |
|
|
52
52
|
|---|---|
|
|
53
|
+
| n8n Cloud | Supported from 4.0.0 |
|
|
53
54
|
| Self-hosted n8n 1.x and 2.x | Supported |
|
|
54
|
-
| n8n
|
|
55
|
-
| n8n verified community nodes | Not eligible |
|
|
55
|
+
| n8n verified community nodes | Eligible from 4.0.0. Passes n8n's verification scanner with no findings |
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Version 4.0.0 uses no local disk. The vault lives in workflow storage and audit records go to the Report output. Upgrading from 1.x? Read [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md). The 1.x line stays installable with `npm install n8n-nodes-redactor@v1-legacy`.
|
|
58
58
|
|
|
59
59
|
## Operations
|
|
60
60
|
|
|
@@ -65,7 +65,7 @@ The persistent vault, encrypted vault files and JSONL audit log write to the loc
|
|
|
65
65
|
| Detect | Assessing data before deciding | Reports what personal data exists without changing anything |
|
|
66
66
|
| Verify | After redaction | Rescans the output and returns PASS or FAIL with any leaked values |
|
|
67
67
|
| Classify | Governance and routing | Labels data PUBLIC, INTERNAL, CONFIDENTIAL or RESTRICTED |
|
|
68
|
-
| Purge | Erasure requests and cleanup | Deletes vault sessions from memory or
|
|
68
|
+
| Purge | Erasure requests and cleanup | Deletes vault sessions from memory or workflow storage, supporting GDPR Article 17 |
|
|
69
69
|
| Stats | Debugging | Lists active vault sessions with entry counts and TTL |
|
|
70
70
|
|
|
71
71
|
The recommended production chain looks like this.
|
|
@@ -109,19 +109,33 @@ Data source > Detect > Redact > LLM > Verify > Restore > Output > Purge
|
|
|
109
109
|
|---|---|
|
|
110
110
|
| Deduplication | The same value gets the same token everywhere, so the model keeps relationships between fields |
|
|
111
111
|
| Field targeting | Allowlist or denylist paths such as `user.email`, `*.phone` or `metadata.*` |
|
|
112
|
-
| Vault |
|
|
113
|
-
| Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant |
|
|
112
|
+
| Vault | Workflow Storage by default, so Restore works in later production runs of the same workflow, or In-Memory. Sessions expire after 60 minutes by default |
|
|
113
|
+
| Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant, kept in a Redactor Vault credential. See [Vault encryption](#vault-encryption) |
|
|
114
114
|
| Confidence threshold | Each match carries a 0.0 to 1.0 score. Checksum validated patterns score 0.95 |
|
|
115
115
|
| Allow and deny lists | Values to never redact, such as your support address, and values to always redact, such as project codenames |
|
|
116
116
|
| Prompt injection check | 25+ patterns, including instruction overrides, delimiter escapes, encoded payloads and zero width characters. Returns BLOCK, FLAG or CLEAN |
|
|
117
117
|
| Audit report | A second output with hits by category and pattern |
|
|
118
|
-
| Audit log |
|
|
118
|
+
| Audit log | Turn on Audit Log to get an `audit` record (counts, labels, timing, never values) on the Report output. Send it to any database, sheet or SIEM node |
|
|
119
119
|
| ROPA record | Turn on Include Audit Report, then Include GDPR Art.30 ROPA Record, and fill in the controller details |
|
|
120
120
|
| Presidio | Optional Microsoft Presidio container for names and organisations in free text. See [docs/PRESIDIO_SETUP.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/PRESIDIO_SETUP.md). Falls back to built-in patterns when unreachable |
|
|
121
|
-
| Per-tenant profiles |
|
|
121
|
+
| Per-tenant profiles | Turn on Use Tenant Profile and paste JSON. Validated on every run. Fields are listed in [docs/MIGRATION.md](https://github.com/mjmirza/n8n-nodes-redactor/blob/master/docs/MIGRATION.md#tenant-profile-fields) |
|
|
122
122
|
|
|
123
123
|
This node reduces what reaches a model. It is not legal advice and does not on its own make a workflow GDPR or HIPAA compliant.
|
|
124
124
|
|
|
125
|
+
## Vault encryption
|
|
126
|
+
|
|
127
|
+
Workflow Storage keeps sessions with the workflow. To encrypt them, create a **Redactor Vault** credential with a passphrase of at least 12 characters and attach it to both the Redact and the Restore node. n8n stores credentials encrypted and apart from the workflow, so the passphrase never sits next to the data it protects. Give each tenant its own credential.
|
|
128
|
+
|
|
129
|
+
The older Vault Encryption Passphrase field still works so workflows saved with it keep restoring, but it is saved in plain text inside the workflow. When both are set, the credential wins.
|
|
130
|
+
|
|
131
|
+
Restore needs the same passphrase that Redact used. If Redact meets a session ID that a different passphrase wrote, it stops with an error instead of overwriting it.
|
|
132
|
+
|
|
133
|
+
### Concurrent runs
|
|
134
|
+
|
|
135
|
+
n8n saves workflow storage as one block at the end of each production run and does not merge it. If two runs of the same workflow overlap, the one that finishes last wins. A session created by the other run can be lost, and a session purged by the other run can come back until its TTL expires. The node cannot prevent this from inside n8n. To stay safe, keep the Session TTL short, avoid overlapping runs of a workflow that uses Purge (for example by limiting production concurrency on your instance), or use In-Memory storage when Redact and Restore run in the same execution.
|
|
136
|
+
|
|
137
|
+
In-Memory sessions are kept per workflow and are never visible to another workflow.
|
|
138
|
+
|
|
125
139
|
## For developers
|
|
126
140
|
|
|
127
141
|
| Area | Detail |
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RedactorVaultApi = void 0;
|
|
4
|
+
class RedactorVaultApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'redactorVaultApi';
|
|
7
|
+
this.displayName = 'Redactor Vault API';
|
|
8
|
+
this.icon = { light: 'file:redactorVault.svg', dark: 'file:redactorVault.dark.svg' };
|
|
9
|
+
this.documentationUrl = 'https://github.com/mjmirza/n8n-nodes-redactor#vault-encryption';
|
|
10
|
+
this.properties = [
|
|
11
|
+
{
|
|
12
|
+
displayName: 'Vault Passphrase',
|
|
13
|
+
name: 'passphrase',
|
|
14
|
+
type: 'string',
|
|
15
|
+
typeOptions: { password: true },
|
|
16
|
+
default: '',
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Encrypts stored sessions with AES-256-GCM. Use at least 12 characters, and a different passphrase for each tenant. Sessions written with this passphrase cannot be read without it.',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.RedactorVaultApi = RedactorVaultApi;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF6B6B" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#FFA42E" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60" viewBox="16 15 370 370"><g transform="translate(0,400) scale(0.1,-0.1)"><path fill="#FF5757" d="M1880 3520 c-200 -204 -414 -321 -714 -390 l-99 -23 6 -171 c6 -183 24 -288 71 -431 55 -163 163 -360 258 -469 34 -40 34 -40 68 18 145 254 391 434 638 467 416 56 762 -273 883 -841 23 -108 34 -413 17 -485 -17 -70 105 200 146 325 116 350 145 599 146 1221 l0 346 -162 6 c-420 16 -743 151 -1016 424 -68 68 -124 123 -125 122 -1 0 -54 -54 -117 -119z"/><path fill="#F79410" d="M703 2628 c6 -546 24 -707 113 -1015 177 -610 573 -1066 1070 -1232 113 -38 115 -38 224 -1 193 64 391 184 472 285 339 421 307 1184 -66 1532 -157 147 -364 176 -528 72 -51 -33 -118 -109 -82 -94 101 42 155 43 241 5 254 -112 199 -491 -85 -601 -265 -103 -614 59 -864 401 -206 281 -312 593 -325 958 l-6 152 -84 0 -85 0 5 -462z"/></g></svg>
|
|
@@ -9,7 +9,9 @@ const classification_1 = require("./classification");
|
|
|
9
9
|
const injection_1 = require("./injection");
|
|
10
10
|
const audit_1 = require("./audit");
|
|
11
11
|
const presidio_1 = require("./presidio");
|
|
12
|
+
const profiles_1 = require("./profiles");
|
|
12
13
|
const ropa_1 = require("./ropa");
|
|
14
|
+
const runtime_1 = require("./runtime");
|
|
13
15
|
const RECOMMENDED_PATTERNS = [
|
|
14
16
|
'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
|
|
15
17
|
'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
|
|
@@ -56,8 +58,34 @@ class Redactor {
|
|
|
56
58
|
name: 'Redactor',
|
|
57
59
|
},
|
|
58
60
|
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
59
|
-
|
|
61
|
+
credentials: [
|
|
62
|
+
{
|
|
63
|
+
name: 'redactorVaultApi',
|
|
64
|
+
required: false,
|
|
65
|
+
testedBy: 'redactorVaultPassphrase',
|
|
66
|
+
displayOptions: {
|
|
67
|
+
show: {
|
|
68
|
+
operation: ['redact', 'restore', 'stats', 'purge'],
|
|
69
|
+
vaultStorage: ['workflow', 'file'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
outputs: `={{((parameters) => (parameters["includeReport"] || parameters["auditLog"]) ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
|
|
60
75
|
properties: [
|
|
76
|
+
{
|
|
77
|
+
displayName: 'Resource',
|
|
78
|
+
name: 'resource',
|
|
79
|
+
type: 'options',
|
|
80
|
+
noDataExpression: true,
|
|
81
|
+
options: [
|
|
82
|
+
{
|
|
83
|
+
name: 'Data',
|
|
84
|
+
value: 'data',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
default: 'data',
|
|
88
|
+
},
|
|
61
89
|
{
|
|
62
90
|
displayName: 'Operation',
|
|
63
91
|
name: 'operation',
|
|
@@ -500,7 +528,7 @@ class Redactor {
|
|
|
500
528
|
},
|
|
501
529
|
},
|
|
502
530
|
default: false,
|
|
503
|
-
description: 'Whether to
|
|
531
|
+
description: 'Whether to add an audit record (counts, labels, severity, timing, no PII values) to the Report output. Store it with any node, for example a database or a file. Supports GDPR Art.30, HIPAA, SOX and PCI DSS record-keeping.',
|
|
504
532
|
},
|
|
505
533
|
{
|
|
506
534
|
displayName: 'Enhanced NLP Detection (Optional)',
|
|
@@ -561,31 +589,17 @@ class Redactor {
|
|
|
561
589
|
},
|
|
562
590
|
},
|
|
563
591
|
options: [
|
|
564
|
-
{
|
|
565
|
-
name: 'File-Based (Recommended, Persistent)',
|
|
566
|
-
value: 'file',
|
|
567
|
-
},
|
|
568
592
|
{
|
|
569
593
|
name: 'In-Memory (Fast, Lost on Restart)',
|
|
570
594
|
value: 'memory',
|
|
571
595
|
},
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
},
|
|
576
|
-
{
|
|
577
|
-
displayName: 'Vault Directory',
|
|
578
|
-
name: 'vaultDir',
|
|
579
|
-
type: 'string',
|
|
580
|
-
displayOptions: {
|
|
581
|
-
show: {
|
|
582
|
-
operation: ['redact', 'restore', 'stats', 'purge'],
|
|
583
|
-
vaultStorage: ['file'],
|
|
596
|
+
{
|
|
597
|
+
name: 'Workflow Storage (Recommended, Persistent)',
|
|
598
|
+
value: 'workflow',
|
|
584
599
|
},
|
|
585
|
-
|
|
586
|
-
default: '',
|
|
587
|
-
|
|
588
|
-
description: 'Custom directory for file-based vault storage',
|
|
600
|
+
],
|
|
601
|
+
default: 'workflow',
|
|
602
|
+
description: 'Workflow storage keeps sessions with the workflow, so Redact and Restore nodes in the same workflow share them and they survive restarts. n8n saves it after production runs of an active workflow. In-memory is faster but lost on restart and capped at 1000 sessions.',
|
|
589
603
|
},
|
|
590
604
|
{
|
|
591
605
|
displayName: 'Vault Encryption Passphrase',
|
|
@@ -597,11 +611,11 @@ class Redactor {
|
|
|
597
611
|
displayOptions: {
|
|
598
612
|
show: {
|
|
599
613
|
operation: ['redact', 'restore', 'stats', 'purge'],
|
|
600
|
-
vaultStorage: ['file'],
|
|
614
|
+
vaultStorage: ['workflow', 'file'],
|
|
601
615
|
},
|
|
602
616
|
},
|
|
603
617
|
default: '',
|
|
604
|
-
description: '
|
|
618
|
+
description: 'Deprecated. This field is saved in plain text inside the workflow. Attach a Redactor Vault credential instead, which n8n stores encrypted and which takes priority. Kept so workflows saved with it still restore.',
|
|
605
619
|
},
|
|
606
620
|
{
|
|
607
621
|
displayName: 'Session TTL (Minutes)',
|
|
@@ -615,6 +629,32 @@ class Redactor {
|
|
|
615
629
|
default: 60,
|
|
616
630
|
description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
|
|
617
631
|
},
|
|
632
|
+
{
|
|
633
|
+
displayName: 'Use Tenant Profile',
|
|
634
|
+
name: 'useTenantProfile',
|
|
635
|
+
type: 'boolean',
|
|
636
|
+
displayOptions: {
|
|
637
|
+
show: {
|
|
638
|
+
operation: ['redact'],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
default: false,
|
|
642
|
+
description: 'Whether to apply per-client settings from a JSON profile on top of this node',
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
displayName: 'Tenant Profile (JSON)',
|
|
646
|
+
name: 'tenantProfile',
|
|
647
|
+
type: 'json',
|
|
648
|
+
required: true,
|
|
649
|
+
displayOptions: {
|
|
650
|
+
show: {
|
|
651
|
+
operation: ['redact'],
|
|
652
|
+
useTenantProfile: [true],
|
|
653
|
+
},
|
|
654
|
+
},
|
|
655
|
+
default: '{}',
|
|
656
|
+
description: 'Optional per-client settings that override this node: enabledPatterns, customPatterns, confidenceThreshold, redactionMode, dedup, fieldMode, fieldRules, allowList, denyList, retentionMinutes, auditLog. Unknown fields are rejected.',
|
|
657
|
+
},
|
|
618
658
|
{
|
|
619
659
|
displayName: 'Purge Scope',
|
|
620
660
|
name: 'purgeScope',
|
|
@@ -713,12 +753,25 @@ class Redactor {
|
|
|
713
753
|
},
|
|
714
754
|
],
|
|
715
755
|
};
|
|
756
|
+
this.methods = {
|
|
757
|
+
credentialTest: {
|
|
758
|
+
async redactorVaultPassphrase(credential) {
|
|
759
|
+
var _a, _b;
|
|
760
|
+
const passphrase = String((_b = (_a = credential.data) === null || _a === void 0 ? void 0 : _a.passphrase) !== null && _b !== void 0 ? _b : '');
|
|
761
|
+
if (passphrase.length < 12) {
|
|
762
|
+
return { status: 'Error', message: 'Use a passphrase of at least 12 characters.' };
|
|
763
|
+
}
|
|
764
|
+
return { status: 'OK', message: 'Passphrase accepted.' };
|
|
765
|
+
},
|
|
766
|
+
},
|
|
767
|
+
};
|
|
716
768
|
}
|
|
717
769
|
async execute() {
|
|
718
770
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
719
771
|
const items = this.getInputData();
|
|
720
772
|
const allPaired = items.map((_, i) => ({ item: i }));
|
|
721
773
|
const operation = this.getNodeParameter('operation', 0);
|
|
774
|
+
const startTime = Date.now();
|
|
722
775
|
(0, presidio_1.resetCircuitBreaker)();
|
|
723
776
|
if (operation === 'detect' || operation === 'verify') {
|
|
724
777
|
const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
|
|
@@ -734,6 +787,7 @@ class Redactor {
|
|
|
734
787
|
fieldRules: [],
|
|
735
788
|
fieldMode: 'all',
|
|
736
789
|
skipSemantic: operation === 'verify',
|
|
790
|
+
scanNumbers: true,
|
|
737
791
|
};
|
|
738
792
|
const allHits = [];
|
|
739
793
|
for (let i = 0; i < items.length; i++) {
|
|
@@ -759,8 +813,21 @@ class Redactor {
|
|
|
759
813
|
includeReport = this.getNodeParameter('includeReport', 0, false);
|
|
760
814
|
}
|
|
761
815
|
catch { }
|
|
762
|
-
|
|
763
|
-
|
|
816
|
+
let auditLog = false;
|
|
817
|
+
try {
|
|
818
|
+
auditLog = this.getNodeParameter('auditLog', 0, false);
|
|
819
|
+
}
|
|
820
|
+
catch { }
|
|
821
|
+
if (includeReport || auditLog) {
|
|
822
|
+
const reportJson = includeReport
|
|
823
|
+
? { ...summary, hits: report.hits }
|
|
824
|
+
: {};
|
|
825
|
+
if (auditLog) {
|
|
826
|
+
reportJson.audit = (0, audit_1.buildAuditEntry)('DETECT', 'token', allHits, items.length, Date.now() - startTime, enabledPatterns.length, false, (0, classification_1.classifyData)(allHits));
|
|
827
|
+
}
|
|
828
|
+
if (!includeReport)
|
|
829
|
+
returnData.push({ json: summary, pairedItem: allPaired });
|
|
830
|
+
return [returnData, [{ json: reportJson, pairedItem: allPaired }]];
|
|
764
831
|
}
|
|
765
832
|
returnData.push({ json: summary, pairedItem: allPaired });
|
|
766
833
|
return [returnData];
|
|
@@ -818,16 +885,10 @@ class Redactor {
|
|
|
818
885
|
pairedItem: allPaired,
|
|
819
886
|
}]];
|
|
820
887
|
}
|
|
821
|
-
const vaultStorage = this.getNodeParameter('vaultStorage', 0, '
|
|
822
|
-
const
|
|
823
|
-
|
|
824
|
-
try {
|
|
825
|
-
vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
|
|
826
|
-
}
|
|
827
|
-
catch { }
|
|
828
|
-
const vault = (0, vault_1.createVault)(vaultStorage, vaultDir || undefined, vaultPassphrase || undefined);
|
|
888
|
+
const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
|
|
889
|
+
const vaultPassphrase = vaultStorage === 'memory' ? '' : await (0, runtime_1.resolvePassphrase)(this);
|
|
890
|
+
const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined, (0, runtime_1.workflowNamespace)(this));
|
|
829
891
|
if (operation === 'stats') {
|
|
830
|
-
vault.cleanup();
|
|
831
892
|
const sessions = vault.listSessions();
|
|
832
893
|
const totalEntries = sessions.reduce((sum, s) => sum + s.entryCount, 0);
|
|
833
894
|
return [[
|
|
@@ -857,7 +918,7 @@ class Redactor {
|
|
|
857
918
|
}
|
|
858
919
|
}
|
|
859
920
|
if (operation === 'redact') {
|
|
860
|
-
|
|
921
|
+
let mode = this.getNodeParameter('redactionMode', 0, 'token');
|
|
861
922
|
const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
|
|
862
923
|
let enabledPatterns;
|
|
863
924
|
if (detectionScope === 'all') {
|
|
@@ -869,17 +930,29 @@ class Redactor {
|
|
|
869
930
|
else {
|
|
870
931
|
enabledPatterns = this.getNodeParameter('piiTypes', 0);
|
|
871
932
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
933
|
+
let dedup = this.getNodeParameter('dedup', 0, true);
|
|
934
|
+
let fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
|
|
935
|
+
let sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
|
|
875
936
|
const includeReport = this.getNodeParameter('includeReport', 0, false);
|
|
876
937
|
const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
|
|
877
|
-
|
|
938
|
+
let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
|
|
878
939
|
const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
|
|
879
|
-
|
|
940
|
+
let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
|
|
880
941
|
field: r.field,
|
|
881
942
|
mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
|
|
882
943
|
}));
|
|
944
|
+
const presidioRequester = async ({ method, url, body, timeout }) => {
|
|
945
|
+
const response = await this.helpers.httpRequest({
|
|
946
|
+
method: method,
|
|
947
|
+
url,
|
|
948
|
+
body,
|
|
949
|
+
json: true,
|
|
950
|
+
timeout,
|
|
951
|
+
returnFullResponse: true,
|
|
952
|
+
ignoreHttpStatusErrors: true,
|
|
953
|
+
});
|
|
954
|
+
return { statusCode: response.statusCode, body: response.body };
|
|
955
|
+
};
|
|
883
956
|
let enablePresidio = false;
|
|
884
957
|
let presidioUrl = '';
|
|
885
958
|
let presidioLanguage = 'en';
|
|
@@ -893,17 +966,63 @@ class Redactor {
|
|
|
893
966
|
catch {
|
|
894
967
|
}
|
|
895
968
|
const allowListRaw = this.getNodeParameter('allowList', 0, {});
|
|
896
|
-
|
|
969
|
+
let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
|
|
897
970
|
value: e.value,
|
|
898
971
|
type: (e.type || 'contains'),
|
|
899
972
|
}));
|
|
900
973
|
const denyListRaw = this.getNodeParameter('denyList', 0, {});
|
|
901
|
-
|
|
974
|
+
let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
|
|
902
975
|
value: e.value,
|
|
903
976
|
type: (e.type || 'exact'),
|
|
904
977
|
}));
|
|
905
|
-
|
|
906
|
-
|
|
978
|
+
let confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
|
|
979
|
+
const hasReportOutput = includeReport || this.getNodeParameter('auditLog', 0, false);
|
|
980
|
+
let auditLog = this.getNodeParameter('auditLog', 0, false);
|
|
981
|
+
const useTenantProfile = this.getNodeParameter('useTenantProfile', 0, false);
|
|
982
|
+
const profileRaw = useTenantProfile ? this.getNodeParameter('tenantProfile', 0, '') : '';
|
|
983
|
+
if (profileRaw !== '' && profileRaw !== undefined && profileRaw !== null) {
|
|
984
|
+
let profile;
|
|
985
|
+
try {
|
|
986
|
+
profile = (0, profiles_1.parseTenantProfile)(profileRaw, (0, patterns_1.getPatternOptions)().map((o) => o.value));
|
|
987
|
+
}
|
|
988
|
+
catch (error) {
|
|
989
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
|
|
990
|
+
description: 'Fix the Tenant Profile (JSON) field, or leave it empty to use the node settings.',
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
if (profile.enabledPatterns)
|
|
994
|
+
enabledPatterns = profile.enabledPatterns;
|
|
995
|
+
if (profile.customPatterns)
|
|
996
|
+
customPatterns = profile.customPatterns;
|
|
997
|
+
if (profile.confidenceThreshold !== undefined)
|
|
998
|
+
confidenceThreshold = profile.confidenceThreshold;
|
|
999
|
+
if (profile.redactionMode)
|
|
1000
|
+
mode = profile.redactionMode;
|
|
1001
|
+
if (profile.dedup !== undefined)
|
|
1002
|
+
dedup = profile.dedup;
|
|
1003
|
+
if (profile.fieldMode)
|
|
1004
|
+
fieldMode = profile.fieldMode;
|
|
1005
|
+
if (profile.fieldRules)
|
|
1006
|
+
fieldRules = profile.fieldRules;
|
|
1007
|
+
if (profile.allowList)
|
|
1008
|
+
allowList = profile.allowList;
|
|
1009
|
+
if (profile.denyList)
|
|
1010
|
+
denyList = profile.denyList;
|
|
1011
|
+
if (profile.retentionMinutes !== undefined)
|
|
1012
|
+
sessionTtl = profile.retentionMinutes;
|
|
1013
|
+
if (profile.auditLog !== undefined)
|
|
1014
|
+
auditLog = profile.auditLog;
|
|
1015
|
+
}
|
|
1016
|
+
(0, runtime_1.assertUserRegexes)(this, customPatterns, [
|
|
1017
|
+
{ name: 'Allow list', entries: allowList },
|
|
1018
|
+
{ name: 'Deny list', entries: denyList },
|
|
1019
|
+
]);
|
|
1020
|
+
try {
|
|
1021
|
+
vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
1025
|
+
}
|
|
907
1026
|
const ctx = {
|
|
908
1027
|
enabledPatterns,
|
|
909
1028
|
customPatterns,
|
|
@@ -916,6 +1035,7 @@ class Redactor {
|
|
|
916
1035
|
confidenceThreshold,
|
|
917
1036
|
presidioUrl: enablePresidio ? presidioUrl : undefined,
|
|
918
1037
|
presidioLanguage: enablePresidio ? presidioLanguage : undefined,
|
|
1038
|
+
presidioRequest: enablePresidio ? presidioRequester : undefined,
|
|
919
1039
|
};
|
|
920
1040
|
const allHits = [];
|
|
921
1041
|
const returnData = [];
|
|
@@ -941,23 +1061,16 @@ class Redactor {
|
|
|
941
1061
|
for (let i = 0; i < items.length; i++) {
|
|
942
1062
|
const result = (0, injection_1.scanForInjection)(items[i].json);
|
|
943
1063
|
if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
|
|
944
|
-
injectionResult = result;
|
|
1064
|
+
injectionResult = (0, runtime_1.sanitizeInjection)(result);
|
|
945
1065
|
}
|
|
946
1066
|
}
|
|
947
1067
|
}
|
|
948
1068
|
}
|
|
949
1069
|
catch {
|
|
950
1070
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
const startTime = Date.now();
|
|
955
|
-
const classification = (0, classification_1.classifyData)(allHits);
|
|
956
|
-
(0, audit_1.writeAuditLog)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, classification, sessionId);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
catch {
|
|
960
|
-
}
|
|
1071
|
+
const auditEntry = auditLog && hasReportOutput
|
|
1072
|
+
? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId)
|
|
1073
|
+
: undefined;
|
|
961
1074
|
if (injectionResult && injectionResult.detected) {
|
|
962
1075
|
returnData.push({
|
|
963
1076
|
json: {
|
|
@@ -966,12 +1079,14 @@ class Redactor {
|
|
|
966
1079
|
pairedItem: allPaired,
|
|
967
1080
|
});
|
|
968
1081
|
}
|
|
969
|
-
if (
|
|
970
|
-
const report = (0, engine_1.buildReport)(sessionId, allHits);
|
|
971
|
-
if (
|
|
1082
|
+
if (hasReportOutput) {
|
|
1083
|
+
const report = includeReport ? (0, engine_1.buildReport)(sessionId, allHits) : {};
|
|
1084
|
+
if (auditEntry)
|
|
1085
|
+
report.audit = auditEntry;
|
|
1086
|
+
if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
|
|
972
1087
|
const d = this.getNodeParameter('ropa', 0, {});
|
|
973
1088
|
const list = (v) => String(v !== null && v !== void 0 ? v : '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
974
|
-
report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase,
|
|
1089
|
+
report.ropa = (0, ropa_1.generateRopaRecord)(allHits, (0, classification_1.classifyData)(allHits), items.length, enabledPatterns.length, mode, sessionTtl, !!vaultPassphrase, auditLog, { name: String((_e = d.controllerName) !== null && _e !== void 0 ? _e : ''), contactEmail: String((_f = d.controllerContact) !== null && _f !== void 0 ? _f : ''), dpo: d.dpo ? String(d.dpo) : undefined }, list(d.purposes), String((_g = d.legalBasis) !== null && _g !== void 0 ? _g : 'legitimate_interest'), list(d.dataSubjects), list(d.recipients), []);
|
|
975
1090
|
}
|
|
976
1091
|
return [returnData, [{ json: report, pairedItem: allPaired }]];
|
|
977
1092
|
}
|
|
@@ -981,8 +1096,8 @@ class Redactor {
|
|
|
981
1096
|
const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
|
|
982
1097
|
const session = vault.getSession(sessionId);
|
|
983
1098
|
if (!session) {
|
|
984
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}". Ensure a Redact node ran first with the same Session ID
|
|
985
|
-
description: 'Testing step by step? Each "Execute step" gets a new execution ID,
|
|
1099
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `No vault found for session "${sessionId}" that this passphrase can open. Ensure a Redact node ran first with the same Session ID, vault storage type and passphrase.`, {
|
|
1100
|
+
description: 'Testing step by step? Each "Execute step" gets a new execution ID, and n8n saves workflow storage only after production runs of an active workflow. Run the whole workflow, or set the same fixed Session ID (for example "test-1") on both nodes. Restore only works in the workflow that ran Redact.',
|
|
986
1101
|
});
|
|
987
1102
|
}
|
|
988
1103
|
const returnData = [];
|
|
@@ -33,23 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
37
|
-
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const MAX_FILE_SIZE = 104857600;
|
|
41
|
-
const DEFAULT_AUDIT_DIR = path.join(process.env.HOME || '/tmp', '.n8n', 'pii-audit');
|
|
42
|
-
function generateEventId() {
|
|
43
|
-
const bytes = new Array(16).fill(0).map(() => Math.floor(Math.random() * 256));
|
|
44
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
45
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
46
|
-
const hex = bytes.map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
47
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
48
|
-
}
|
|
49
|
-
function getLogFilePath(auditDir) {
|
|
50
|
-
const today = new Date().toISOString().split('T')[0];
|
|
51
|
-
return path.join(auditDir, `pii-audit-${today}.jsonl`);
|
|
52
|
-
}
|
|
36
|
+
exports.buildAuditEntry = buildAuditEntry;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
53
38
|
function determineSeverity(classificationLevel, totalHits = 0) {
|
|
54
39
|
if (classificationLevel === 'RESTRICTED' || totalHits >= 50)
|
|
55
40
|
return 'CRITICAL';
|
|
@@ -59,87 +44,29 @@ function determineSeverity(classificationLevel, totalHits = 0) {
|
|
|
59
44
|
return 'MEDIUM';
|
|
60
45
|
return 'LOW';
|
|
61
46
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const hitsByCategory = {};
|
|
69
|
-
const hitsByPattern = {};
|
|
70
|
-
for (const hit of hits) {
|
|
71
|
-
hitsByCategory[hit.category] = (hitsByCategory[hit.category] || 0) + 1;
|
|
72
|
-
hitsByPattern[hit.patternLabel] = (hitsByPattern[hit.patternLabel] || 0) + 1;
|
|
73
|
-
}
|
|
74
|
-
const entry = {
|
|
75
|
-
version: '1.0',
|
|
76
|
-
timestamp: new Date().toISOString(),
|
|
77
|
-
eventId: generateEventId(),
|
|
78
|
-
severity: determineSeverity(classification === null || classification === void 0 ? void 0 : classification.level, hits.length),
|
|
79
|
-
action,
|
|
80
|
-
mode,
|
|
81
|
-
status: 'SUCCESS',
|
|
82
|
-
totalHits: hits.length,
|
|
83
|
-
hitsByCategory,
|
|
84
|
-
hitsByPattern,
|
|
85
|
-
classificationLevel: classification === null || classification === void 0 ? void 0 : classification.level,
|
|
86
|
-
inputItemCount,
|
|
87
|
-
processingTimeMs,
|
|
88
|
-
patternsUsed,
|
|
89
|
-
presidioEnabled,
|
|
90
|
-
sessionId,
|
|
91
|
-
};
|
|
92
|
-
const logFile = getLogFilePath(dir);
|
|
93
|
-
try {
|
|
94
|
-
if (fs.existsSync(logFile)) {
|
|
95
|
-
const stats = fs.statSync(logFile);
|
|
96
|
-
if (stats.size >= MAX_FILE_SIZE) {
|
|
97
|
-
let seq = 1;
|
|
98
|
-
while (fs.existsSync(`${logFile}.${seq}`))
|
|
99
|
-
seq++;
|
|
100
|
-
fs.renameSync(logFile, `${logFile}.${seq}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
}
|
|
106
|
-
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n', 'utf-8');
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function readAuditLog(startDate, endDate, auditDir) {
|
|
112
|
-
try {
|
|
113
|
-
const dir = auditDir || DEFAULT_AUDIT_DIR;
|
|
114
|
-
if (!fs.existsSync(dir))
|
|
115
|
-
return [];
|
|
116
|
-
const files = fs.readdirSync(dir)
|
|
117
|
-
.filter((f) => f.startsWith('pii-audit-') && f.endsWith('.jsonl'))
|
|
118
|
-
.sort();
|
|
119
|
-
const entries = [];
|
|
120
|
-
for (const file of files) {
|
|
121
|
-
const fileDate = file.replace('pii-audit-', '').replace('.jsonl', '');
|
|
122
|
-
if (startDate && fileDate < startDate)
|
|
123
|
-
continue;
|
|
124
|
-
if (endDate && fileDate > endDate)
|
|
125
|
-
continue;
|
|
126
|
-
try {
|
|
127
|
-
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
128
|
-
const lines = content.split('\n').filter((l) => l.trim().length > 0);
|
|
129
|
-
for (const line of lines) {
|
|
130
|
-
try {
|
|
131
|
-
entries.push(JSON.parse(line));
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
catch {
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return entries;
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return [];
|
|
47
|
+
function buildAuditEntry(action, mode, hits, inputItemCount, processingTimeMs, patternsUsed, presidioEnabled, classification, sessionId) {
|
|
48
|
+
const hitsByCategory = {};
|
|
49
|
+
const hitsByPattern = {};
|
|
50
|
+
for (const hit of hits) {
|
|
51
|
+
hitsByCategory[hit.category] = (hitsByCategory[hit.category] || 0) + 1;
|
|
52
|
+
hitsByPattern[hit.patternLabel] = (hitsByPattern[hit.patternLabel] || 0) + 1;
|
|
144
53
|
}
|
|
54
|
+
return {
|
|
55
|
+
version: '2.0',
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
eventId: crypto.randomUUID(),
|
|
58
|
+
severity: determineSeverity(classification === null || classification === void 0 ? void 0 : classification.level, hits.length),
|
|
59
|
+
action,
|
|
60
|
+
mode,
|
|
61
|
+
status: 'SUCCESS',
|
|
62
|
+
totalHits: hits.length,
|
|
63
|
+
hitsByCategory,
|
|
64
|
+
hitsByPattern,
|
|
65
|
+
classificationLevel: classification === null || classification === void 0 ? void 0 : classification.level,
|
|
66
|
+
inputItemCount,
|
|
67
|
+
processingTimeMs,
|
|
68
|
+
patternsUsed,
|
|
69
|
+
presidioEnabled,
|
|
70
|
+
sessionId,
|
|
71
|
+
};
|
|
145
72
|
}
|