n8n-nodes-redactor 1.11.1 → 4.0.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 +9 -9
- package/dist/nodes/Redactor/Redactor.node.js +137 -53
- package/dist/nodes/Redactor/audit.d.ts +1 -2
- package/dist/nodes/Redactor/audit.js +26 -99
- package/dist/nodes/Redactor/engine.js +1 -1
- package/dist/nodes/Redactor/presidio.d.ts +12 -3
- package/dist/nodes/Redactor/presidio.js +30 -84
- package/dist/nodes/Redactor/profiles.d.ts +5 -8
- package/dist/nodes/Redactor/profiles.js +119 -101
- package/dist/nodes/Redactor/types.d.ts +3 -1
- package/dist/nodes/Redactor/vault.d.ts +10 -6
- package/dist/nodes/Redactor/vault.js +104 -146
- package/package.json +10 -6
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,16 +109,16 @@ 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 |
|
|
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
113
|
| Vault encryption | AES-256-GCM with scrypt key derivation, one passphrase per tenant |
|
|
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
|
|
|
@@ -9,6 +9,7 @@ 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");
|
|
13
14
|
const RECOMMENDED_PATTERNS = [
|
|
14
15
|
'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
|
|
@@ -56,8 +57,21 @@ class Redactor {
|
|
|
56
57
|
name: 'Redactor',
|
|
57
58
|
},
|
|
58
59
|
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
59
|
-
outputs: `={{((parameters) => parameters["includeReport"] ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
|
|
60
|
+
outputs: `={{((parameters) => (parameters["includeReport"] || parameters["auditLog"]) ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
|
|
60
61
|
properties: [
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Resource',
|
|
64
|
+
name: 'resource',
|
|
65
|
+
type: 'options',
|
|
66
|
+
noDataExpression: true,
|
|
67
|
+
options: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Data',
|
|
70
|
+
value: 'data',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
default: 'data',
|
|
74
|
+
},
|
|
61
75
|
{
|
|
62
76
|
displayName: 'Operation',
|
|
63
77
|
name: 'operation',
|
|
@@ -500,7 +514,7 @@ class Redactor {
|
|
|
500
514
|
},
|
|
501
515
|
},
|
|
502
516
|
default: false,
|
|
503
|
-
description: 'Whether to
|
|
517
|
+
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
518
|
},
|
|
505
519
|
{
|
|
506
520
|
displayName: 'Enhanced NLP Detection (Optional)',
|
|
@@ -561,31 +575,17 @@ class Redactor {
|
|
|
561
575
|
},
|
|
562
576
|
},
|
|
563
577
|
options: [
|
|
564
|
-
{
|
|
565
|
-
name: 'File-Based (Recommended, Persistent)',
|
|
566
|
-
value: 'file',
|
|
567
|
-
},
|
|
568
578
|
{
|
|
569
579
|
name: 'In-Memory (Fast, Lost on Restart)',
|
|
570
580
|
value: 'memory',
|
|
571
581
|
},
|
|
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'],
|
|
582
|
+
{
|
|
583
|
+
name: 'Workflow Storage (Recommended, Persistent)',
|
|
584
|
+
value: 'workflow',
|
|
584
585
|
},
|
|
585
|
-
|
|
586
|
-
default: '',
|
|
587
|
-
|
|
588
|
-
description: 'Custom directory for file-based vault storage',
|
|
586
|
+
],
|
|
587
|
+
default: 'workflow',
|
|
588
|
+
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
589
|
},
|
|
590
590
|
{
|
|
591
591
|
displayName: 'Vault Encryption Passphrase',
|
|
@@ -597,11 +597,11 @@ class Redactor {
|
|
|
597
597
|
displayOptions: {
|
|
598
598
|
show: {
|
|
599
599
|
operation: ['redact', 'restore', 'stats', 'purge'],
|
|
600
|
-
vaultStorage: ['file'],
|
|
600
|
+
vaultStorage: ['workflow', 'file'],
|
|
601
601
|
},
|
|
602
602
|
},
|
|
603
603
|
default: '',
|
|
604
|
-
description: 'Encrypt
|
|
604
|
+
description: 'Encrypt stored sessions at rest with AES-256-GCM. Leave empty for no encryption. When set, sessions cannot be read without this passphrase. Each tenant should use a different passphrase for complete isolation.',
|
|
605
605
|
},
|
|
606
606
|
{
|
|
607
607
|
displayName: 'Session TTL (Minutes)',
|
|
@@ -615,6 +615,32 @@ class Redactor {
|
|
|
615
615
|
default: 60,
|
|
616
616
|
description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
|
|
617
617
|
},
|
|
618
|
+
{
|
|
619
|
+
displayName: 'Use Tenant Profile',
|
|
620
|
+
name: 'useTenantProfile',
|
|
621
|
+
type: 'boolean',
|
|
622
|
+
displayOptions: {
|
|
623
|
+
show: {
|
|
624
|
+
operation: ['redact'],
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
default: false,
|
|
628
|
+
description: 'Whether to apply per-client settings from a JSON profile on top of this node',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
displayName: 'Tenant Profile (JSON)',
|
|
632
|
+
name: 'tenantProfile',
|
|
633
|
+
type: 'json',
|
|
634
|
+
required: true,
|
|
635
|
+
displayOptions: {
|
|
636
|
+
show: {
|
|
637
|
+
operation: ['redact'],
|
|
638
|
+
useTenantProfile: [true],
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
default: '{}',
|
|
642
|
+
description: 'Optional per-client settings that override this node: enabledPatterns, customPatterns, confidenceThreshold, redactionMode, dedup, fieldMode, fieldRules, allowList, denyList, retentionMinutes, auditLog. Unknown fields are rejected.',
|
|
643
|
+
},
|
|
618
644
|
{
|
|
619
645
|
displayName: 'Purge Scope',
|
|
620
646
|
name: 'purgeScope',
|
|
@@ -719,6 +745,7 @@ class Redactor {
|
|
|
719
745
|
const items = this.getInputData();
|
|
720
746
|
const allPaired = items.map((_, i) => ({ item: i }));
|
|
721
747
|
const operation = this.getNodeParameter('operation', 0);
|
|
748
|
+
const startTime = Date.now();
|
|
722
749
|
(0, presidio_1.resetCircuitBreaker)();
|
|
723
750
|
if (operation === 'detect' || operation === 'verify') {
|
|
724
751
|
const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
|
|
@@ -759,8 +786,21 @@ class Redactor {
|
|
|
759
786
|
includeReport = this.getNodeParameter('includeReport', 0, false);
|
|
760
787
|
}
|
|
761
788
|
catch { }
|
|
762
|
-
|
|
763
|
-
|
|
789
|
+
let auditLog = false;
|
|
790
|
+
try {
|
|
791
|
+
auditLog = this.getNodeParameter('auditLog', 0, false);
|
|
792
|
+
}
|
|
793
|
+
catch { }
|
|
794
|
+
if (includeReport || auditLog) {
|
|
795
|
+
const reportJson = includeReport
|
|
796
|
+
? { ...summary, hits: report.hits }
|
|
797
|
+
: {};
|
|
798
|
+
if (auditLog) {
|
|
799
|
+
reportJson.audit = (0, audit_1.buildAuditEntry)('DETECT', 'token', allHits, items.length, Date.now() - startTime, enabledPatterns.length, false, (0, classification_1.classifyData)(allHits));
|
|
800
|
+
}
|
|
801
|
+
if (!includeReport)
|
|
802
|
+
returnData.push({ json: summary, pairedItem: allPaired });
|
|
803
|
+
return [returnData, [{ json: reportJson, pairedItem: allPaired }]];
|
|
764
804
|
}
|
|
765
805
|
returnData.push({ json: summary, pairedItem: allPaired });
|
|
766
806
|
return [returnData];
|
|
@@ -818,14 +858,13 @@ class Redactor {
|
|
|
818
858
|
pairedItem: allPaired,
|
|
819
859
|
}]];
|
|
820
860
|
}
|
|
821
|
-
const vaultStorage = this.getNodeParameter('vaultStorage', 0, '
|
|
822
|
-
const vaultDir = this.getNodeParameter('vaultDir', 0, '');
|
|
861
|
+
const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'workflow');
|
|
823
862
|
let vaultPassphrase = '';
|
|
824
863
|
try {
|
|
825
864
|
vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
|
|
826
865
|
}
|
|
827
866
|
catch { }
|
|
828
|
-
const vault = (0, vault_1.createVault)(vaultStorage,
|
|
867
|
+
const vault = (0, vault_1.createVault)(vaultStorage, vaultStorage === 'memory' ? undefined : this.getWorkflowStaticData('global'), vaultPassphrase || undefined);
|
|
829
868
|
if (operation === 'stats') {
|
|
830
869
|
vault.cleanup();
|
|
831
870
|
const sessions = vault.listSessions();
|
|
@@ -857,7 +896,7 @@ class Redactor {
|
|
|
857
896
|
}
|
|
858
897
|
}
|
|
859
898
|
if (operation === 'redact') {
|
|
860
|
-
|
|
899
|
+
let mode = this.getNodeParameter('redactionMode', 0, 'token');
|
|
861
900
|
const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
|
|
862
901
|
let enabledPatterns;
|
|
863
902
|
if (detectionScope === 'all') {
|
|
@@ -869,17 +908,29 @@ class Redactor {
|
|
|
869
908
|
else {
|
|
870
909
|
enabledPatterns = this.getNodeParameter('piiTypes', 0);
|
|
871
910
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
911
|
+
let dedup = this.getNodeParameter('dedup', 0, true);
|
|
912
|
+
let fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
|
|
913
|
+
let sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
|
|
875
914
|
const includeReport = this.getNodeParameter('includeReport', 0, false);
|
|
876
915
|
const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
|
|
877
|
-
|
|
916
|
+
let customPatterns = (_a = customPatternsRaw.patterns) !== null && _a !== void 0 ? _a : [];
|
|
878
917
|
const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
|
|
879
|
-
|
|
918
|
+
let fieldRules = ((_b = fieldRulesRaw.rules) !== null && _b !== void 0 ? _b : []).map((r) => ({
|
|
880
919
|
field: r.field,
|
|
881
920
|
mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
|
|
882
921
|
}));
|
|
922
|
+
const presidioRequester = async ({ method, url, body, timeout }) => {
|
|
923
|
+
const response = await this.helpers.httpRequest({
|
|
924
|
+
method: method,
|
|
925
|
+
url,
|
|
926
|
+
body,
|
|
927
|
+
json: true,
|
|
928
|
+
timeout,
|
|
929
|
+
returnFullResponse: true,
|
|
930
|
+
ignoreHttpStatusErrors: true,
|
|
931
|
+
});
|
|
932
|
+
return { statusCode: response.statusCode, body: response.body };
|
|
933
|
+
};
|
|
883
934
|
let enablePresidio = false;
|
|
884
935
|
let presidioUrl = '';
|
|
885
936
|
let presidioLanguage = 'en';
|
|
@@ -893,16 +944,53 @@ class Redactor {
|
|
|
893
944
|
catch {
|
|
894
945
|
}
|
|
895
946
|
const allowListRaw = this.getNodeParameter('allowList', 0, {});
|
|
896
|
-
|
|
947
|
+
let allowList = ((_c = allowListRaw.entries) !== null && _c !== void 0 ? _c : []).map((e) => ({
|
|
897
948
|
value: e.value,
|
|
898
949
|
type: (e.type || 'contains'),
|
|
899
950
|
}));
|
|
900
951
|
const denyListRaw = this.getNodeParameter('denyList', 0, {});
|
|
901
|
-
|
|
952
|
+
let denyList = ((_d = denyListRaw.entries) !== null && _d !== void 0 ? _d : []).map((e) => ({
|
|
902
953
|
value: e.value,
|
|
903
954
|
type: (e.type || 'exact'),
|
|
904
955
|
}));
|
|
905
|
-
|
|
956
|
+
let confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
|
|
957
|
+
const hasReportOutput = includeReport || this.getNodeParameter('auditLog', 0, false);
|
|
958
|
+
let auditLog = this.getNodeParameter('auditLog', 0, false);
|
|
959
|
+
const useTenantProfile = this.getNodeParameter('useTenantProfile', 0, false);
|
|
960
|
+
const profileRaw = useTenantProfile ? this.getNodeParameter('tenantProfile', 0, '') : '';
|
|
961
|
+
if (profileRaw !== '' && profileRaw !== undefined && profileRaw !== null) {
|
|
962
|
+
let profile;
|
|
963
|
+
try {
|
|
964
|
+
profile = (0, profiles_1.parseTenantProfile)(profileRaw, (0, patterns_1.getPatternOptions)().map((o) => o.value));
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message, {
|
|
968
|
+
description: 'Fix the Tenant Profile (JSON) field, or leave it empty to use the node settings.',
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
if (profile.enabledPatterns)
|
|
972
|
+
enabledPatterns = profile.enabledPatterns;
|
|
973
|
+
if (profile.customPatterns)
|
|
974
|
+
customPatterns = profile.customPatterns;
|
|
975
|
+
if (profile.confidenceThreshold !== undefined)
|
|
976
|
+
confidenceThreshold = profile.confidenceThreshold;
|
|
977
|
+
if (profile.redactionMode)
|
|
978
|
+
mode = profile.redactionMode;
|
|
979
|
+
if (profile.dedup !== undefined)
|
|
980
|
+
dedup = profile.dedup;
|
|
981
|
+
if (profile.fieldMode)
|
|
982
|
+
fieldMode = profile.fieldMode;
|
|
983
|
+
if (profile.fieldRules)
|
|
984
|
+
fieldRules = profile.fieldRules;
|
|
985
|
+
if (profile.allowList)
|
|
986
|
+
allowList = profile.allowList;
|
|
987
|
+
if (profile.denyList)
|
|
988
|
+
denyList = profile.denyList;
|
|
989
|
+
if (profile.retentionMinutes !== undefined)
|
|
990
|
+
sessionTtl = profile.retentionMinutes;
|
|
991
|
+
if (profile.auditLog !== undefined)
|
|
992
|
+
auditLog = profile.auditLog;
|
|
993
|
+
}
|
|
906
994
|
vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
|
|
907
995
|
const ctx = {
|
|
908
996
|
enabledPatterns,
|
|
@@ -916,6 +1004,7 @@ class Redactor {
|
|
|
916
1004
|
confidenceThreshold,
|
|
917
1005
|
presidioUrl: enablePresidio ? presidioUrl : undefined,
|
|
918
1006
|
presidioLanguage: enablePresidio ? presidioLanguage : undefined,
|
|
1007
|
+
presidioRequest: enablePresidio ? presidioRequester : undefined,
|
|
919
1008
|
};
|
|
920
1009
|
const allHits = [];
|
|
921
1010
|
const returnData = [];
|
|
@@ -948,16 +1037,9 @@ class Redactor {
|
|
|
948
1037
|
}
|
|
949
1038
|
catch {
|
|
950
1039
|
}
|
|
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
|
-
}
|
|
1040
|
+
const auditEntry = auditLog && hasReportOutput
|
|
1041
|
+
? (0, audit_1.buildAuditEntry)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, (0, classification_1.classifyData)(allHits), sessionId)
|
|
1042
|
+
: undefined;
|
|
961
1043
|
if (injectionResult && injectionResult.detected) {
|
|
962
1044
|
returnData.push({
|
|
963
1045
|
json: {
|
|
@@ -966,12 +1048,14 @@ class Redactor {
|
|
|
966
1048
|
pairedItem: allPaired,
|
|
967
1049
|
});
|
|
968
1050
|
}
|
|
969
|
-
if (
|
|
970
|
-
const report = (0, engine_1.buildReport)(sessionId, allHits);
|
|
971
|
-
if (
|
|
1051
|
+
if (hasReportOutput) {
|
|
1052
|
+
const report = includeReport ? (0, engine_1.buildReport)(sessionId, allHits) : {};
|
|
1053
|
+
if (auditEntry)
|
|
1054
|
+
report.audit = auditEntry;
|
|
1055
|
+
if (includeReport && this.getNodeParameter('includeRopa', 0, false)) {
|
|
972
1056
|
const d = this.getNodeParameter('ropa', 0, {});
|
|
973
1057
|
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,
|
|
1058
|
+
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
1059
|
}
|
|
976
1060
|
return [returnData, [{ json: report, pairedItem: allPaired }]];
|
|
977
1061
|
}
|
|
@@ -982,7 +1066,7 @@ class Redactor {
|
|
|
982
1066
|
const session = vault.getSession(sessionId);
|
|
983
1067
|
if (!session) {
|
|
984
1068
|
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 and vault storage type.`, {
|
|
985
|
-
description: 'Testing step by step? Each "Execute step" gets a new execution ID,
|
|
1069
|
+
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
1070
|
});
|
|
987
1071
|
}
|
|
988
1072
|
const returnData = [];
|
|
@@ -18,5 +18,4 @@ export interface AuditLogEntry {
|
|
|
18
18
|
presidioEnabled: boolean;
|
|
19
19
|
sessionId?: string;
|
|
20
20
|
}
|
|
21
|
-
export declare function
|
|
22
|
-
export declare function readAuditLog(startDate?: string, endDate?: string, auditDir?: string): AuditLogEntry[];
|
|
21
|
+
export declare function buildAuditEntry(action: string, mode: RedactionMode | string, hits: RedactionHit[], inputItemCount: number, processingTimeMs: number, patternsUsed: number, presidioEnabled: boolean, classification?: ClassificationLabel, sessionId?: string): AuditLogEntry;
|
|
@@ -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
|
}
|
|
@@ -624,7 +624,7 @@ async function enhanceWithPresidio(data, ctx, vault, sessionId, hits, itemIndex)
|
|
|
624
624
|
if (TOKEN_RE.test(value))
|
|
625
625
|
continue;
|
|
626
626
|
try {
|
|
627
|
-
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000);
|
|
627
|
+
const entities = await (0, presidio_1.analyzeWithPresidio)(ctx.presidioUrl, value, ctx.presidioLanguage || 'en', 0.4, 10000, ctx.presidioRequest);
|
|
628
628
|
if (entities.length === 0)
|
|
629
629
|
continue;
|
|
630
630
|
const sorted = entities.sort((a, b) => b.start - a.start);
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export type PresidioRequester = (options: {
|
|
2
|
+
method: 'GET' | 'POST';
|
|
3
|
+
url: string;
|
|
4
|
+
body?: object;
|
|
5
|
+
timeout: number;
|
|
6
|
+
}) => Promise<{
|
|
7
|
+
statusCode: number;
|
|
8
|
+
body: unknown;
|
|
9
|
+
}>;
|
|
1
10
|
export interface PresidioEntity {
|
|
2
11
|
entity_type: string;
|
|
3
12
|
start: number;
|
|
@@ -20,8 +29,8 @@ export interface PresidioEntity {
|
|
|
20
29
|
};
|
|
21
30
|
}
|
|
22
31
|
export declare function resetCircuitBreaker(): void;
|
|
23
|
-
export declare function checkPresidioHealth(baseUrl: string): Promise<boolean>;
|
|
24
|
-
export declare function analyzeWithPresidio(baseUrl: string, text: string, language?: string, scoreThreshold?: number, timeoutMs?: number): Promise<PresidioEntity[]>;
|
|
32
|
+
export declare function checkPresidioHealth(baseUrl: string, request?: PresidioRequester): Promise<boolean>;
|
|
33
|
+
export declare function analyzeWithPresidio(baseUrl: string, text: string, language?: string, scoreThreshold?: number, timeoutMs?: number, request?: PresidioRequester): Promise<PresidioEntity[]>;
|
|
25
34
|
export declare function getRedactorLabel(presidioEntityType: string): string;
|
|
26
35
|
export declare function getRedactorCategory(presidioEntityType: string): string;
|
|
27
|
-
export declare function getPresidioEntities(baseUrl: string): Promise<string[]>;
|
|
36
|
+
export declare function getPresidioEntities(baseUrl: string, request?: PresidioRequester): Promise<string[]>;
|