n8n-nodes-redactor 3.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.
Files changed (61) hide show
  1. package/LICENSE +42 -0
  2. package/README.dev.md +153 -0
  3. package/README.md +443 -0
  4. package/README.npm.md +443 -0
  5. package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
  6. package/dist/nodes/PiiRedactor/PiiRedactor.node.js +1093 -0
  7. package/dist/nodes/PiiRedactor/__tests__/encryption.test.d.ts +1 -0
  8. package/dist/nodes/PiiRedactor/__tests__/encryption.test.js +200 -0
  9. package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
  10. package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
  11. package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
  12. package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
  13. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
  14. package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
  15. package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
  16. package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
  17. package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
  18. package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
  19. package/dist/nodes/PiiRedactor/__tests__/phase3.test.d.ts +1 -0
  20. package/dist/nodes/PiiRedactor/__tests__/phase3.test.js +275 -0
  21. package/dist/nodes/PiiRedactor/__tests__/phase4.test.d.ts +1 -0
  22. package/dist/nodes/PiiRedactor/__tests__/phase4.test.js +184 -0
  23. package/dist/nodes/PiiRedactor/__tests__/presidio.test.d.ts +1 -0
  24. package/dist/nodes/PiiRedactor/__tests__/presidio.test.js +170 -0
  25. package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
  26. package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
  27. package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
  28. package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
  29. package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
  30. package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
  31. package/dist/nodes/PiiRedactor/audit.d.ts +48 -0
  32. package/dist/nodes/PiiRedactor/audit.js +192 -0
  33. package/dist/nodes/PiiRedactor/classification.d.ts +33 -0
  34. package/dist/nodes/PiiRedactor/classification.js +118 -0
  35. package/dist/nodes/PiiRedactor/context.d.ts +57 -0
  36. package/dist/nodes/PiiRedactor/context.js +260 -0
  37. package/dist/nodes/PiiRedactor/encryption.d.ts +45 -0
  38. package/dist/nodes/PiiRedactor/encryption.js +158 -0
  39. package/dist/nodes/PiiRedactor/engine.d.ts +23 -0
  40. package/dist/nodes/PiiRedactor/engine.js +888 -0
  41. package/dist/nodes/PiiRedactor/injection.d.ts +46 -0
  42. package/dist/nodes/PiiRedactor/injection.js +425 -0
  43. package/dist/nodes/PiiRedactor/names.d.ts +25 -0
  44. package/dist/nodes/PiiRedactor/names.js +188 -0
  45. package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
  46. package/dist/nodes/PiiRedactor/patterns.js +1742 -0
  47. package/dist/nodes/PiiRedactor/presidio.d.ts +77 -0
  48. package/dist/nodes/PiiRedactor/presidio.js +264 -0
  49. package/dist/nodes/PiiRedactor/profiles.d.ts +47 -0
  50. package/dist/nodes/PiiRedactor/profiles.js +139 -0
  51. package/dist/nodes/PiiRedactor/pseudonymize.d.ts +20 -0
  52. package/dist/nodes/PiiRedactor/pseudonymize.js +203 -0
  53. package/dist/nodes/PiiRedactor/redact.png +0 -0
  54. package/dist/nodes/PiiRedactor/redact.svg +3 -0
  55. package/dist/nodes/PiiRedactor/ropa.d.ts +63 -0
  56. package/dist/nodes/PiiRedactor/ropa.js +70 -0
  57. package/dist/nodes/PiiRedactor/types.d.ts +82 -0
  58. package/dist/nodes/PiiRedactor/types.js +3 -0
  59. package/dist/nodes/PiiRedactor/vault.d.ts +61 -0
  60. package/dist/nodes/PiiRedactor/vault.js +352 -0
  61. package/package.json +87 -0
@@ -0,0 +1,1093 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PiiRedactor = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const patterns_1 = require("./patterns");
6
+ const vault_1 = require("./vault");
7
+ const engine_1 = require("./engine");
8
+ const classification_1 = require("./classification");
9
+ const injection_1 = require("./injection");
10
+ const audit_1 = require("./audit");
11
+ const presidio_1 = require("./presidio");
12
+ /**
13
+ * Curated high-precision pattern set. These patterns have low false-positive
14
+ * rates and are safe to enable by default. Excludes broad patterns like
15
+ * bare digit-only matches (postalCodeDE, postalCodeAT, bsnNL, etc.)
16
+ */
17
+ const RECOMMENDED_PATTERNS = [
18
+ // Contact
19
+ 'email', 'phone', 'phoneDE', 'phoneUK', 'phoneAT', 'phoneCH', 'phoneFR',
20
+ 'phoneNL', 'phoneES', 'phoneIT', 'phoneAU', 'phoneIN', 'phoneBR',
21
+ // Identity - high precision
22
+ 'personName', 'ssn', 'itinUS', 'sinCA', 'ninoUK', 'passportUS', 'passportEU',
23
+ 'passportDE', 'nationalIdDE', 'nhsNumber', 'taxIdUS', 'taxIdDE',
24
+ 'sozialversicherungDE', 'ahvCH', 'nationalIdFR', 'codiceFiscaleIT',
25
+ 'dniES', 'nieES', 'nifES', 'peselPL', 'bsnNL', 'hetuFI', 'ppsIE',
26
+ 'nricSG', 'panIN', 'cpfBR', 'driverLicenseCtx', 'employeeIdCtx',
27
+ 'fullNameCtx', 'mothersMaidenName', 'passportCtx',
28
+ // Financial - validated
29
+ 'creditCard', 'amex', 'iban', 'bic', 'vatEU', 'abaRouting', 'sortCodeUK',
30
+ 'cardExpiry', 'cvvCtx', 'bankAccountCtx', 'insurancePolicyCtx',
31
+ 'salaryCtx', 'loyaltyNumberCtx',
32
+ // Network
33
+ 'ipv4', 'ipv6', 'macAddress', 'ipv4Port', 'url', 'privateIp',
34
+ // Location - validated formats
35
+ 'postalCodeUK', 'postalCodeNL', 'postalCodePL', 'postalCodeCA',
36
+ 'postalCodeJP', 'gpsCoordinates',
37
+ 'addressDE', 'addressLabeledDE', 'addressLabeledEN', 'addressLabeledFR',
38
+ 'addressEUStreetNumber',
39
+ // Dates
40
+ 'dateSlash', 'dateDash', 'dateDot', 'dateOfBirth',
41
+ // Medical
42
+ 'medicalRecordNumber', 'deaNumber', 'npiNumber', 'rxNumber',
43
+ 'healthPlanCtx', 'bloodTypeCtx',
44
+ // Enterprise secrets
45
+ 'internalHostname', 'uncPath', 'ldapDN', 'adUsername',
46
+ 'dbConnectionString', 'connStringKV',
47
+ 'awsAccessKey', 'gcpApiKey', 'stripeKey', 'openaiKey', 'githubToken',
48
+ 'slackToken', 'bearerToken', 'jwtToken', 'pemPrivateKey', 'sshPublicKey',
49
+ 'genericSecret', 'azureKey', 'slackWebhook',
50
+ 'sendgridKey', 'twilioKey', 'anthropicKey', 'gitlabToken',
51
+ 'x509Certificate', 'pgpPrivateKey', 'envSecrets',
52
+ // Crypto
53
+ 'bitcoinAddress', 'ethereumAddress',
54
+ // Vehicle
55
+ 'vin',
56
+ // Biometric
57
+ 'uuid', 'imei', 'iccid',
58
+ ];
59
+ class PiiRedactor {
60
+ constructor() {
61
+ this.description = {
62
+ displayName: 'Redactor',
63
+ name: 'redactor',
64
+ icon: 'file:redact.png',
65
+ group: ['transform'],
66
+ version: 1,
67
+ subtitle: '={{$parameter["operation"]}}',
68
+ usableAsTool: true,
69
+ description: 'Redact and remove sensitive data, PII, personal information, emails, phone numbers, addresses, IBAN, credit cards, names before sending to LLM. Anonymize, mask, sanitize, scrub customer data. Restore original values after. GDPR HIPAA CCPA compliant. Data privacy, data protection, data masking, data anonymization.',
70
+ defaults: {
71
+ name: 'Redactor',
72
+ },
73
+ inputs: ['main'],
74
+ // @ts-ignore dynamic outputs using n8n's function pattern (same as Switch node)
75
+ outputs: `={{((parameters) => parameters["includeReport"] ? [{ type: "main", displayName: "Data" }, { type: "main", displayName: "Report" }] : [{ type: "main", displayName: "Data" }])($parameter)}}`,
76
+ properties: [
77
+ // ═══════════════════════════════════════
78
+ // OPERATION
79
+ // ═══════════════════════════════════════
80
+ {
81
+ displayName: 'Operation',
82
+ name: 'operation',
83
+ type: 'options',
84
+ noDataExpression: true,
85
+ options: [
86
+ {
87
+ name: 'Redact',
88
+ value: 'redact',
89
+ description: 'Detect and replace PII with tokens/masks/hashes',
90
+ },
91
+ {
92
+ name: 'Restore',
93
+ value: 'restore',
94
+ description: 'Replace tokens back with original PII values from vault',
95
+ },
96
+ {
97
+ name: 'Detect (Scan Only)',
98
+ value: 'detect',
99
+ description: 'Scan for PII without modifying the data. Returns a report of what was found.',
100
+ },
101
+ {
102
+ name: 'Verify',
103
+ value: 'verify',
104
+ description: 'Re-scan redacted data to confirm no PII leaked through. Returns pass/fail.',
105
+ },
106
+ {
107
+ name: 'Classify',
108
+ value: 'classify',
109
+ description: 'Assign sensitivity labels (PUBLIC/INTERNAL/CONFIDENTIAL/RESTRICTED) based on PII found.',
110
+ },
111
+ {
112
+ name: 'Purge',
113
+ value: 'purge',
114
+ description: 'Delete vault sessions. Supports GDPR right to erasure.',
115
+ },
116
+ {
117
+ name: 'Stats',
118
+ value: 'stats',
119
+ description: 'List active vault sessions and entry counts',
120
+ },
121
+ ],
122
+ default: 'redact',
123
+ },
124
+ // ═══════════════════════════════════════
125
+ // SESSION ID (shared between Redact & Restore)
126
+ // ═══════════════════════════════════════
127
+ {
128
+ displayName: 'Session ID',
129
+ name: 'sessionId',
130
+ type: 'string',
131
+ default: '={{$execution.id}}',
132
+ description: 'Unique ID linking a Redact and Restore step. Defaults to the current execution ID. Use the same value in both the Redact and Restore nodes.',
133
+ required: true,
134
+ displayOptions: {
135
+ show: {
136
+ operation: ['redact', 'restore'],
137
+ },
138
+ },
139
+ },
140
+ {
141
+ displayName: 'Session ID to Purge',
142
+ name: 'purgeSessionId',
143
+ type: 'string',
144
+ default: '',
145
+ description: 'The session ID to purge. Leave empty is not needed when purging all.',
146
+ displayOptions: {
147
+ show: {
148
+ operation: ['purge'],
149
+ purgeScope: ['specific'],
150
+ },
151
+ },
152
+ },
153
+ // ═══════════════════════════════════════
154
+ // REDACTION MODE
155
+ // ═══════════════════════════════════════
156
+ {
157
+ displayName: 'Redaction Mode',
158
+ name: 'redactionMode',
159
+ type: 'options',
160
+ displayOptions: {
161
+ show: {
162
+ operation: ['redact'],
163
+ },
164
+ },
165
+ options: [
166
+ {
167
+ name: 'Token (Reversible)',
168
+ value: 'token',
169
+ description: 'Replace PII with [EMAIL_0], [PHONE_1] etc. Restore via vault later. Best for LLM workflows.',
170
+ },
171
+ {
172
+ name: 'Mask (Partial)',
173
+ value: 'mask',
174
+ description: 'Show partial values: j***@e***.com, ****-****-****-1234. NOT reversible.',
175
+ },
176
+ {
177
+ name: 'Hash (Deterministic)',
178
+ value: 'hash',
179
+ description: 'Replace with truncated SHA-256 hash. Deterministic but NOT reversible.',
180
+ },
181
+ {
182
+ name: 'Redact (Full Removal)',
183
+ value: 'redact',
184
+ description: 'Replace with [REDACTED]. NOT reversible.',
185
+ },
186
+ {
187
+ name: 'Pseudonymize (Fake Realistic Data)',
188
+ value: 'pseudonymize',
189
+ description: 'Replace with realistic fake data (names, emails, phones). Preserves format. Reversible via vault. GDPR-compliant pseudonymization.',
190
+ },
191
+ {
192
+ name: 'Blackout (Visual Black Bars)',
193
+ value: 'blackout',
194
+ description: 'Replace with black bars (████████). Same length as original. NOT reversible. Like physical document redaction.',
195
+ },
196
+ {
197
+ name: 'Remove (Complete Deletion)',
198
+ value: 'remove',
199
+ description: 'Completely remove PII, leaving no trace. NOT reversible. The value is replaced with an empty string.',
200
+ },
201
+ ],
202
+ default: 'token',
203
+ },
204
+ // ═══════════════════════════════════════
205
+ // DETECTION SCOPE
206
+ // ═══════════════════════════════════════
207
+ {
208
+ displayName: 'Detection Scope',
209
+ name: 'detectionScope',
210
+ type: 'options',
211
+ displayOptions: {
212
+ show: {
213
+ operation: ['redact'],
214
+ },
215
+ },
216
+ options: [
217
+ {
218
+ name: 'Recommended (High Precision)',
219
+ value: 'recommended',
220
+ description: 'Curated set of high-precision patterns. Best for most workflows. Minimal false positives.',
221
+ },
222
+ {
223
+ name: 'All Patterns (Maximum Coverage)',
224
+ value: 'all',
225
+ description: 'All 200+ patterns including broad ones. May produce false positives on short numbers.',
226
+ },
227
+ {
228
+ name: 'Select Specific Types',
229
+ value: 'select',
230
+ description: 'Choose which specific data types to detect.',
231
+ },
232
+ ],
233
+ default: 'recommended',
234
+ description: 'Detect all sensitive data types automatically, or select specific types to scan for.',
235
+ },
236
+ // ═══════════════════════════════════════
237
+ // PII TYPE SELECTION (only shown when scope is 'select')
238
+ // ═══════════════════════════════════════
239
+ {
240
+ displayName: 'PII Types to Detect',
241
+ name: 'piiTypes',
242
+ type: 'multiOptions',
243
+ displayOptions: {
244
+ show: {
245
+ operation: ['redact'],
246
+ detectionScope: ['select'],
247
+ },
248
+ },
249
+ options: (0, patterns_1.getPatternOptions)(),
250
+ default: [
251
+ 'email',
252
+ 'phone',
253
+ 'phoneDE',
254
+ 'phoneUK',
255
+ 'personName',
256
+ 'ssn',
257
+ 'ninoUK',
258
+ 'creditCard',
259
+ 'iban',
260
+ 'bic',
261
+ 'ipv4',
262
+ 'privateIp',
263
+ 'addressDE',
264
+ 'addressLabeledEN',
265
+ 'addressLabeledDE',
266
+ 'awsAccessKey',
267
+ 'genericSecret',
268
+ 'dbConnectionString',
269
+ 'jwtToken',
270
+ 'pemPrivateKey',
271
+ ],
272
+ description: 'Select which PII types to detect. Each type uses optimized regex with validation where possible.',
273
+ },
274
+ // ═══════════════════════════════════════
275
+ // DEDUPLICATION
276
+ // ═══════════════════════════════════════
277
+ {
278
+ displayName: 'Deduplicate',
279
+ name: 'dedup',
280
+ type: 'boolean',
281
+ displayOptions: {
282
+ show: {
283
+ operation: ['redact'],
284
+ redactionMode: ['token'],
285
+ },
286
+ },
287
+ default: true,
288
+ description: 'When enabled, the same PII value always gets the same token. This preserves relationships in the data (e.g. if an email appears in 3 fields, all 3 get [EMAIL_0]).',
289
+ },
290
+ // ═══════════════════════════════════════
291
+ // FIELD TARGETING
292
+ // ═══════════════════════════════════════
293
+ {
294
+ displayName: 'Field Targeting',
295
+ name: 'fieldMode',
296
+ type: 'options',
297
+ displayOptions: {
298
+ show: {
299
+ operation: ['redact'],
300
+ },
301
+ },
302
+ options: [
303
+ {
304
+ name: 'All Fields',
305
+ value: 'all',
306
+ description: 'Scan every field in the JSON data',
307
+ },
308
+ {
309
+ name: 'Allowlist -Only These Fields',
310
+ value: 'allowlist',
311
+ description: 'Only scan the fields listed below',
312
+ },
313
+ {
314
+ name: 'Denylist -Skip These Fields',
315
+ value: 'denylist',
316
+ description: 'Scan everything except the fields listed below',
317
+ },
318
+ ],
319
+ default: 'all',
320
+ description: 'Control which JSON fields get scanned. Use allowlist to target specific fields, or denylist to skip fields like internal IDs.',
321
+ },
322
+ {
323
+ displayName: 'Field Rules',
324
+ name: 'fieldRules',
325
+ type: 'fixedCollection',
326
+ typeOptions: {
327
+ multipleValues: true,
328
+ },
329
+ displayOptions: {
330
+ show: {
331
+ operation: ['redact'],
332
+ fieldMode: ['allowlist', 'denylist'],
333
+ },
334
+ },
335
+ default: {},
336
+ options: [
337
+ {
338
+ name: 'rules',
339
+ displayName: 'Rule',
340
+ values: [
341
+ {
342
+ displayName: 'Field Path',
343
+ name: 'field',
344
+ type: 'string',
345
+ default: '',
346
+ placeholder: 'e.g. user.email, contacts[*].phone, *.address',
347
+ description: 'JSON path to target. Supports dot notation and wildcards: "user.email", "*.phone", "items[*].name"',
348
+ },
349
+ ],
350
+ },
351
+ ],
352
+ },
353
+ // ═══════════════════════════════════════
354
+ // CONFIDENCE THRESHOLD
355
+ // ═══════════════════════════════════════
356
+ {
357
+ displayName: 'Confidence Threshold',
358
+ name: 'confidenceThreshold',
359
+ type: 'number',
360
+ displayOptions: {
361
+ show: {
362
+ operation: ['redact'],
363
+ },
364
+ },
365
+ typeOptions: {
366
+ minValue: 0,
367
+ maxValue: 1,
368
+ numberPrecision: 2,
369
+ },
370
+ default: 0,
371
+ description: 'Only redact detections with confidence score above this threshold (0.0 to 1.0). Set to 0 to redact everything. Set to 0.7 to only redact high-confidence matches. Checksum-validated patterns score 0.95, regex-only patterns score 0.60-0.85.',
372
+ },
373
+ // ═══════════════════════════════════════
374
+ // ALLOW LIST (bypass specific values)
375
+ // ═══════════════════════════════════════
376
+ {
377
+ displayName: 'Allow List (Never Redact)',
378
+ name: 'allowList',
379
+ type: 'fixedCollection',
380
+ typeOptions: {
381
+ multipleValues: true,
382
+ },
383
+ displayOptions: {
384
+ show: {
385
+ operation: ['redact'],
386
+ },
387
+ },
388
+ default: {},
389
+ options: [
390
+ {
391
+ name: 'entries',
392
+ displayName: 'Entry',
393
+ values: [
394
+ {
395
+ displayName: 'Value',
396
+ name: 'value',
397
+ type: 'string',
398
+ default: '',
399
+ placeholder: 'e.g. @mycompany.com, +1-800-555-0100, support@acme.com',
400
+ description: 'Value to never redact, even if a pattern matches it',
401
+ },
402
+ {
403
+ displayName: 'Match Type',
404
+ name: 'type',
405
+ type: 'options',
406
+ options: [
407
+ { name: 'Exact Match', value: 'exact' },
408
+ { name: 'Contains', value: 'contains' },
409
+ { name: 'Regex', value: 'regex' },
410
+ ],
411
+ default: 'contains',
412
+ description: 'How to match the value',
413
+ },
414
+ ],
415
+ },
416
+ ],
417
+ description: 'Values that should NEVER be redacted. Use this for company email domains, public phone numbers, or known-safe identifiers.',
418
+ },
419
+ // ═══════════════════════════════════════
420
+ // DENY LIST (always redact specific values)
421
+ // ═══════════════════════════════════════
422
+ {
423
+ displayName: 'Deny List (Always Redact)',
424
+ name: 'denyList',
425
+ type: 'fixedCollection',
426
+ typeOptions: {
427
+ multipleValues: true,
428
+ },
429
+ displayOptions: {
430
+ show: {
431
+ operation: ['redact'],
432
+ },
433
+ },
434
+ default: {},
435
+ options: [
436
+ {
437
+ name: 'entries',
438
+ displayName: 'Entry',
439
+ values: [
440
+ {
441
+ displayName: 'Value',
442
+ name: 'value',
443
+ type: 'string',
444
+ default: '',
445
+ placeholder: 'e.g. Project Falcon, INTERNAL-2024-SECRET',
446
+ description: 'Value to ALWAYS redact, even if no pattern matches it',
447
+ },
448
+ {
449
+ displayName: 'Match Type',
450
+ name: 'type',
451
+ type: 'options',
452
+ options: [
453
+ { name: 'Exact Match', value: 'exact' },
454
+ { name: 'Contains', value: 'contains' },
455
+ { name: 'Regex', value: 'regex' },
456
+ ],
457
+ default: 'exact',
458
+ description: 'How to match the value',
459
+ },
460
+ ],
461
+ },
462
+ ],
463
+ description: 'Values that should ALWAYS be redacted, regardless of whether any pattern matches. Use for project codenames, internal secrets, or business-specific sensitive terms.',
464
+ },
465
+ // ═══════════════════════════════════════
466
+ // CUSTOM PATTERNS
467
+ // ═══════════════════════════════════════
468
+ {
469
+ displayName: 'Custom Patterns',
470
+ name: 'customPatterns',
471
+ type: 'fixedCollection',
472
+ typeOptions: {
473
+ multipleValues: true,
474
+ },
475
+ displayOptions: {
476
+ show: {
477
+ operation: ['redact'],
478
+ },
479
+ },
480
+ default: {},
481
+ options: [
482
+ {
483
+ name: 'patterns',
484
+ displayName: 'Pattern',
485
+ values: [
486
+ {
487
+ displayName: 'Label',
488
+ name: 'label',
489
+ type: 'string',
490
+ default: '',
491
+ placeholder: 'e.g. ORDER_ID, SKU, TICKET',
492
+ description: 'Label for the redacted placeholder',
493
+ },
494
+ {
495
+ displayName: 'Regex',
496
+ name: 'regex',
497
+ type: 'string',
498
+ default: '',
499
+ placeholder: 'e.g. ORD-\\d{6}, SKU-[A-Z0-9]{8}',
500
+ description: 'Regular expression to match this pattern',
501
+ },
502
+ {
503
+ displayName: 'Category',
504
+ name: 'category',
505
+ type: 'options',
506
+ options: [
507
+ { name: 'Identity', value: 'identity' },
508
+ { name: 'Financial', value: 'financial' },
509
+ { name: 'Contact', value: 'contact' },
510
+ { name: 'Network', value: 'network' },
511
+ { name: 'Location', value: 'location' },
512
+ { name: 'Medical', value: 'medical' },
513
+ { name: 'Other', value: 'other' },
514
+ ],
515
+ default: 'identity',
516
+ description: 'Category for audit reporting',
517
+ },
518
+ ],
519
+ },
520
+ ],
521
+ description: 'Add your own regex patterns for business-specific identifiers (order IDs, SKUs, ticket numbers, etc.)',
522
+ },
523
+ // ═══════════════════════════════════════
524
+ // PROMPT INJECTION DETECTION
525
+ // ═══════════════════════════════════════
526
+ {
527
+ displayName: 'Prompt Injection Detection',
528
+ name: 'promptInjection',
529
+ type: 'boolean',
530
+ displayOptions: {
531
+ show: {
532
+ operation: ['redact'],
533
+ },
534
+ },
535
+ default: false,
536
+ description: 'Scan input data for prompt injection attempts (jailbreaks, instruction overrides, delimiter escapes, encoded payloads) before sending to an LLM. Detected injections are flagged in the output.',
537
+ },
538
+ // ═══════════════════════════════════════
539
+ // AUDIT LOG
540
+ // ═══════════════════════════════════════
541
+ {
542
+ displayName: 'Audit Log',
543
+ name: 'auditLog',
544
+ type: 'boolean',
545
+ displayOptions: {
546
+ show: {
547
+ operation: ['redact', 'detect'],
548
+ },
549
+ },
550
+ default: false,
551
+ description: 'Write a persistent JSONL audit log of all redaction activity. Stored in ~/.n8n/pii-audit/. Compliant with GDPR Art.30, HIPAA, SOX, PCI DSS retention requirements.',
552
+ },
553
+ // ═══════════════════════════════════════
554
+ // ENHANCED NLP DETECTION (Optional Presidio)
555
+ // ═══════════════════════════════════════
556
+ {
557
+ displayName: 'Enhanced NLP Detection (Optional)',
558
+ name: 'enablePresidio',
559
+ type: 'boolean',
560
+ displayOptions: {
561
+ show: {
562
+ operation: ['redact'],
563
+ },
564
+ },
565
+ default: false,
566
+ description: 'Enable NLP-based detection using Microsoft Presidio. Catches person names, locations, and organizations in free text that regex cannot detect. Requires a Presidio Docker container running locally. Setup: docker run -d -p 5002:3000 mcr.microsoft.com/presidio-analyzer',
567
+ },
568
+ {
569
+ displayName: 'Presidio URL',
570
+ name: 'presidioUrl',
571
+ type: 'string',
572
+ displayOptions: {
573
+ show: {
574
+ operation: ['redact'],
575
+ enablePresidio: [true],
576
+ },
577
+ },
578
+ default: 'http://localhost:5002',
579
+ placeholder: 'http://localhost:5002',
580
+ description: 'URL of the Presidio Analyzer service. Default: http://localhost:5002. If using docker-compose, use the service name: http://presidio:3000',
581
+ },
582
+ {
583
+ displayName: 'Presidio Language',
584
+ name: 'presidioLanguage',
585
+ type: 'options',
586
+ displayOptions: {
587
+ show: {
588
+ operation: ['redact'],
589
+ enablePresidio: [true],
590
+ },
591
+ },
592
+ options: [
593
+ { name: 'English', value: 'en' },
594
+ { name: 'German (Deutsch)', value: 'de' },
595
+ { name: 'French (Francais)', value: 'fr' },
596
+ { name: 'Spanish (Espanol)', value: 'es' },
597
+ { name: 'Italian (Italiano)', value: 'it' },
598
+ { name: 'Portuguese (Portugues)', value: 'pt' },
599
+ { name: 'Dutch (Nederlands)', value: 'nl' },
600
+ { name: 'Polish (Polski)', value: 'pl' },
601
+ ],
602
+ default: 'en',
603
+ description: 'Language of the text being analyzed. Presidio uses language-specific NLP models for entity detection.',
604
+ },
605
+ // ═══════════════════════════════════════
606
+ // VAULT SETTINGS
607
+ // ═══════════════════════════════════════
608
+ {
609
+ displayName: 'Vault Storage',
610
+ name: 'vaultStorage',
611
+ type: 'options',
612
+ displayOptions: {
613
+ show: {
614
+ operation: ['redact', 'restore', 'stats', 'purge'],
615
+ },
616
+ },
617
+ options: [
618
+ {
619
+ name: 'File-Based (Recommended, Persistent)',
620
+ value: 'file',
621
+ },
622
+ {
623
+ name: 'In-Memory (Fast, Lost on Restart)',
624
+ value: 'memory',
625
+ },
626
+ ],
627
+ default: 'file',
628
+ description: 'File-based (recommended): Persists to disk, survives restarts, no memory bloat. Stored in ~/.n8n/pii-vault/. In-memory: Faster but lost on restart, capped at 1000 sessions to prevent memory bloat.',
629
+ },
630
+ {
631
+ displayName: 'Vault Directory',
632
+ name: 'vaultDir',
633
+ type: 'string',
634
+ displayOptions: {
635
+ show: {
636
+ operation: ['redact', 'restore', 'stats', 'purge'],
637
+ vaultStorage: ['file'],
638
+ },
639
+ },
640
+ default: '',
641
+ placeholder: '/path/to/vault (default: ~/.n8n/pii-vault/)',
642
+ description: 'Custom directory for file-based vault storage',
643
+ },
644
+ {
645
+ displayName: 'Vault Encryption Passphrase',
646
+ name: 'vaultPassphrase',
647
+ type: 'string',
648
+ typeOptions: {
649
+ password: true,
650
+ },
651
+ displayOptions: {
652
+ show: {
653
+ operation: ['redact', 'restore', 'stats', 'purge'],
654
+ vaultStorage: ['file'],
655
+ },
656
+ },
657
+ default: '',
658
+ description: 'Encrypt vault files at rest with AES-256-GCM. Leave empty for no encryption. When set, vault files cannot be read without this passphrase. Each tenant should use a different passphrase for complete isolation.',
659
+ },
660
+ {
661
+ displayName: 'Session TTL (minutes)',
662
+ name: 'sessionTtl',
663
+ type: 'number',
664
+ displayOptions: {
665
+ show: {
666
+ operation: ['redact'],
667
+ },
668
+ },
669
+ default: 60,
670
+ description: 'Auto-expire vault sessions after this many minutes. Set to 0 for no expiry. Prevents vault from growing unbounded.',
671
+ },
672
+ // ═══════════════════════════════════════
673
+ // PURGE OPTIONS
674
+ // ═══════════════════════════════════════
675
+ {
676
+ displayName: 'Purge Scope',
677
+ name: 'purgeScope',
678
+ type: 'options',
679
+ displayOptions: {
680
+ show: {
681
+ operation: ['purge'],
682
+ },
683
+ },
684
+ options: [
685
+ {
686
+ name: 'Purge All Sessions',
687
+ value: 'all',
688
+ description: 'Delete all vault sessions. GDPR right to erasure.',
689
+ },
690
+ {
691
+ name: 'Purge Specific Session',
692
+ value: 'specific',
693
+ description: 'Delete a specific session by ID.',
694
+ },
695
+ ],
696
+ default: 'all',
697
+ },
698
+ // ═══════════════════════════════════════
699
+ // RESTORE OPTIONS
700
+ // ═══════════════════════════════════════
701
+ {
702
+ displayName: 'Delete Vault After Restore',
703
+ name: 'deleteAfterRestore',
704
+ type: 'boolean',
705
+ displayOptions: {
706
+ show: {
707
+ operation: ['restore'],
708
+ },
709
+ },
710
+ default: true,
711
+ description: 'Delete the vault session after restoring. Disable for multi-step workflows that need multiple restores.',
712
+ },
713
+ // ═══════════════════════════════════════
714
+ // AUDIT REPORT
715
+ // ═══════════════════════════════════════
716
+ {
717
+ displayName: 'Include Audit Report',
718
+ name: 'includeReport',
719
+ type: 'boolean',
720
+ displayOptions: {
721
+ show: {
722
+ operation: ['redact', 'detect'],
723
+ },
724
+ },
725
+ default: false,
726
+ description: 'When enabled, adds a second output with an audit report listing all PII detected, categories, and counts. Useful for compliance logging.',
727
+ },
728
+ ],
729
+ };
730
+ }
731
+ async execute() {
732
+ const items = this.getInputData();
733
+ const operation = this.getNodeParameter('operation', 0);
734
+ // Reset Presidio circuit breaker at start of each execution
735
+ (0, presidio_1.resetCircuitBreaker)();
736
+ // ─── DETECT / VERIFY (zero-config, scans everything) ──
737
+ if (operation === 'detect' || operation === 'verify') {
738
+ // Detect and Verify always scan ALL 216 patterns.
739
+ // No parameters needed. Simple, reliable, no "Could not get parameter".
740
+ const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
741
+ const customPatterns = [];
742
+ const tempVault = (0, vault_1.createVault)('memory');
743
+ const tempSessionId = `${operation}_${Date.now()}`;
744
+ tempVault.getOrCreateSession(tempSessionId, 60000);
745
+ const ctx = {
746
+ enabledPatterns,
747
+ customPatterns,
748
+ mode: 'token',
749
+ dedup: true,
750
+ fieldRules: [],
751
+ fieldMode: 'all',
752
+ // Verify mode: skip semantic field-name detection.
753
+ // Only check if actual PII VALUES leaked through (regex only).
754
+ skipSemantic: operation === 'verify',
755
+ };
756
+ const allHits = [];
757
+ for (let i = 0; i < items.length; i++) {
758
+ const hits = [];
759
+ (0, engine_1.redactValue)(items[i].json, ctx, tempVault, tempSessionId, hits, i);
760
+ allHits.push(...hits);
761
+ }
762
+ tempVault.deleteSession(tempSessionId);
763
+ if (operation === 'detect') {
764
+ // Always return original data + detection summary as last item
765
+ const returnData = items.map((item) => ({ json: item.json }));
766
+ const report = (0, engine_1.buildReport)(tempSessionId, allHits);
767
+ // Summary always appended so user sees results
768
+ const summary = {
769
+ _redactorScan: true,
770
+ status: allHits.length === 0 ? 'CLEAN' : 'PII_FOUND',
771
+ totalPiiFound: allHits.length,
772
+ itemsScanned: items.length,
773
+ hitsByCategory: report.hitsByCategory,
774
+ hitsByPattern: report.hitsByPattern,
775
+ timestamp: report.timestamp,
776
+ };
777
+ let includeReport = false;
778
+ try {
779
+ includeReport = this.getNodeParameter('includeReport', 0, false);
780
+ }
781
+ catch { }
782
+ if (includeReport) {
783
+ // Full report on second output
784
+ return [returnData, [{ json: { ...summary, hits: report.hits } }]];
785
+ }
786
+ // Append summary as last item in single output
787
+ returnData.push({ json: summary });
788
+ return [returnData];
789
+ }
790
+ // Verify
791
+ const passed = allHits.length === 0;
792
+ const verifyReport = (0, engine_1.buildReport)(tempSessionId, allHits);
793
+ return [[{
794
+ json: {
795
+ verified: passed,
796
+ status: passed ? 'PASS' : 'FAIL',
797
+ leaksFound: allHits.length,
798
+ itemsScanned: items.length,
799
+ message: passed
800
+ ? 'No PII detected in values. Data appears properly redacted.'
801
+ : `Found ${allHits.length} potential PII leak(s). Values contain detectable sensitive data.`,
802
+ hitsByCategory: verifyReport.hitsByCategory,
803
+ hitsByPattern: verifyReport.hitsByPattern,
804
+ leaks: allHits.map((h) => ({
805
+ field: h.field,
806
+ patternLabel: h.patternLabel,
807
+ category: h.category,
808
+ itemIndex: h.itemIndex,
809
+ })),
810
+ },
811
+ }]];
812
+ }
813
+ // ─── CLASSIFY (sensitivity labeling) ──
814
+ if (operation === 'classify') {
815
+ const enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
816
+ const tempVault = (0, vault_1.createVault)('memory');
817
+ const tempSessionId = `classify_${Date.now()}`;
818
+ tempVault.getOrCreateSession(tempSessionId, 60000);
819
+ const ctx = {
820
+ enabledPatterns,
821
+ customPatterns: [],
822
+ mode: 'token',
823
+ dedup: true,
824
+ fieldRules: [],
825
+ fieldMode: 'all',
826
+ };
827
+ const allHits = [];
828
+ for (let i = 0; i < items.length; i++) {
829
+ const hits = [];
830
+ (0, engine_1.redactValue)(items[i].json, ctx, tempVault, tempSessionId, hits, i);
831
+ allHits.push(...hits);
832
+ }
833
+ tempVault.deleteSession(tempSessionId);
834
+ const classification = (0, classification_1.classifyData)(allHits);
835
+ return [[{
836
+ json: {
837
+ ...classification,
838
+ itemsScanned: items.length,
839
+ timestamp: new Date().toISOString(),
840
+ },
841
+ }]];
842
+ }
843
+ // All other operations (redact, restore, stats, purge, classify) use vault
844
+ const vaultStorage = this.getNodeParameter('vaultStorage', 0, 'file');
845
+ const vaultDir = this.getNodeParameter('vaultDir', 0, '');
846
+ let vaultPassphrase = '';
847
+ try {
848
+ vaultPassphrase = this.getNodeParameter('vaultPassphrase', 0, '');
849
+ }
850
+ catch { /* not available */ }
851
+ const vault = (0, vault_1.createVault)(vaultStorage, vaultDir || undefined, vaultPassphrase || undefined);
852
+ // ─── STATS ───────────────────────────────
853
+ if (operation === 'stats') {
854
+ vault.cleanup();
855
+ const sessions = vault.listSessions();
856
+ const totalEntries = sessions.reduce((sum, s) => sum + s.entryCount, 0);
857
+ return [[
858
+ {
859
+ json: {
860
+ totalSessions: sessions.length,
861
+ totalEntries,
862
+ vaultStorage: vaultStorage,
863
+ timestamp: new Date().toISOString(),
864
+ sessions: sessions.map((s) => ({
865
+ sessionId: s.sessionId,
866
+ entryCount: s.entryCount,
867
+ createdAt: s.createdAt,
868
+ ttl: s.ttl,
869
+ ttlMinutes: s.ttl > 0 ? Math.round(s.ttl / 60000) : 'no expiry',
870
+ })),
871
+ },
872
+ },
873
+ ]];
874
+ }
875
+ const sessionId = this.getNodeParameter('sessionId', 0);
876
+ if (!sessionId) {
877
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required');
878
+ }
879
+ // ─── REDACT ──────────────────────────────
880
+ if (operation === 'redact') {
881
+ const mode = this.getNodeParameter('redactionMode', 0, 'token');
882
+ const detectionScope = this.getNodeParameter('detectionScope', 0, 'recommended');
883
+ let enabledPatterns;
884
+ if (detectionScope === 'all') {
885
+ enabledPatterns = (0, patterns_1.getPatternOptions)().map((p) => p.value);
886
+ }
887
+ else if (detectionScope === 'recommended') {
888
+ enabledPatterns = RECOMMENDED_PATTERNS;
889
+ }
890
+ else {
891
+ enabledPatterns = this.getNodeParameter('piiTypes', 0);
892
+ }
893
+ const dedup = this.getNodeParameter('dedup', 0, true);
894
+ const fieldMode = this.getNodeParameter('fieldMode', 0, 'all');
895
+ const sessionTtl = this.getNodeParameter('sessionTtl', 0, 60);
896
+ const includeReport = this.getNodeParameter('includeReport', 0, false);
897
+ const customPatternsRaw = this.getNodeParameter('customPatterns', 0, {});
898
+ const customPatterns = customPatternsRaw.patterns ?? [];
899
+ const fieldRulesRaw = this.getNodeParameter('fieldRules', 0, {});
900
+ const fieldRules = (fieldRulesRaw.rules ?? []).map((r) => ({
901
+ field: r.field,
902
+ mode: fieldMode === 'allowlist' ? 'include' : 'exclude',
903
+ }));
904
+ // Presidio NLP integration (optional)
905
+ let enablePresidio = false;
906
+ let presidioUrl = '';
907
+ let presidioLanguage = 'en';
908
+ try {
909
+ enablePresidio = this.getNodeParameter('enablePresidio', 0, false);
910
+ if (enablePresidio) {
911
+ presidioUrl = this.getNodeParameter('presidioUrl', 0, 'http://localhost:5002');
912
+ presidioLanguage = this.getNodeParameter('presidioLanguage', 0, 'en');
913
+ }
914
+ }
915
+ catch {
916
+ // Presidio parameters not available
917
+ }
918
+ // Parse allow list
919
+ const allowListRaw = this.getNodeParameter('allowList', 0, {});
920
+ const allowList = (allowListRaw.entries ?? []).map((e) => ({
921
+ value: e.value,
922
+ type: (e.type || 'contains'),
923
+ }));
924
+ // Parse deny list
925
+ const denyListRaw = this.getNodeParameter('denyList', 0, {});
926
+ const denyList = (denyListRaw.entries ?? []).map((e) => ({
927
+ value: e.value,
928
+ type: (e.type || 'exact'),
929
+ }));
930
+ // Confidence threshold
931
+ const confidenceThreshold = this.getNodeParameter('confidenceThreshold', 0, 0);
932
+ // Initialize vault session
933
+ vault.getOrCreateSession(sessionId, sessionTtl * 60 * 1000);
934
+ const ctx = {
935
+ enabledPatterns,
936
+ customPatterns,
937
+ mode,
938
+ dedup,
939
+ fieldRules,
940
+ fieldMode,
941
+ allowList,
942
+ denyList,
943
+ confidenceThreshold,
944
+ presidioUrl: enablePresidio ? presidioUrl : undefined,
945
+ presidioLanguage: enablePresidio ? presidioLanguage : undefined,
946
+ };
947
+ const allHits = [];
948
+ const returnData = [];
949
+ for (let i = 0; i < items.length; i++) {
950
+ const item = items[i];
951
+ const hits = [];
952
+ let redacted = (0, engine_1.redactValue)(item.json, ctx, vault, sessionId, hits, i);
953
+ // Enhanced NLP detection via Presidio (if enabled)
954
+ if (ctx.presidioUrl) {
955
+ try {
956
+ redacted = await (0, engine_1.enhanceWithPresidio)(redacted, ctx, vault, sessionId, hits, i);
957
+ }
958
+ catch {
959
+ // Presidio unavailable — continue with regex-only results
960
+ }
961
+ }
962
+ returnData.push({ json: redacted });
963
+ allHits.push(...hits);
964
+ }
965
+ // Persist vault
966
+ vault.save(sessionId);
967
+ // Prompt injection detection (if enabled)
968
+ let injectionResult = null;
969
+ try {
970
+ const promptInjection = this.getNodeParameter('promptInjection', 0, false);
971
+ if (promptInjection) {
972
+ for (let i = 0; i < items.length; i++) {
973
+ const result = (0, injection_1.scanForInjection)(items[i].json);
974
+ if (result.detected && (!injectionResult || result.score > injectionResult.score)) {
975
+ injectionResult = result;
976
+ }
977
+ }
978
+ }
979
+ }
980
+ catch {
981
+ // Prompt injection parameter not available
982
+ }
983
+ // Audit log (if enabled)
984
+ try {
985
+ const auditLog = this.getNodeParameter('auditLog', 0, false);
986
+ if (auditLog) {
987
+ const startTime = Date.now();
988
+ const classification = (0, classification_1.classifyData)(allHits);
989
+ (0, audit_1.writeAuditLog)('REDACT', mode, allHits, items.length, Date.now() - startTime, enabledPatterns.length, !!ctx.presidioUrl, classification, sessionId);
990
+ }
991
+ }
992
+ catch {
993
+ // Audit log parameter not available or write failed
994
+ }
995
+ // Build output
996
+ if (injectionResult && injectionResult.detected) {
997
+ // Append injection result to output
998
+ returnData.push({
999
+ json: {
1000
+ _promptInjection: injectionResult,
1001
+ },
1002
+ });
1003
+ }
1004
+ if (includeReport) {
1005
+ const report = (0, engine_1.buildReport)(sessionId, allHits);
1006
+ return [returnData, [{ json: report }]];
1007
+ }
1008
+ return [returnData];
1009
+ }
1010
+ // ─── RESTORE ─────────────────────────────
1011
+ if (operation === 'restore') {
1012
+ const deleteAfterRestore = this.getNodeParameter('deleteAfterRestore', 0, true);
1013
+ let session = vault.getSession(sessionId);
1014
+ let actualSessionId = sessionId;
1015
+ // Safe fallback: if exact session not found and there is EXACTLY ONE
1016
+ // session in the vault, use it. This handles step-by-step testing in
1017
+ // n8n where each "Execute step" creates a new execution ID.
1018
+ // We ONLY fall back when there is no ambiguity (1 session).
1019
+ // With multiple sessions, we refuse to guess to prevent cross-user leakage.
1020
+ if (!session) {
1021
+ const allSessions = vault.listSessions();
1022
+ if (allSessions.length === 1) {
1023
+ actualSessionId = allSessions[0].sessionId;
1024
+ session = vault.getSession(actualSessionId);
1025
+ }
1026
+ }
1027
+ if (!session) {
1028
+ 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.`);
1029
+ }
1030
+ const returnData = [];
1031
+ for (let i = 0; i < items.length; i++) {
1032
+ const item = items[i];
1033
+ const restored = (0, engine_1.restoreValue)(item.json, vault, actualSessionId);
1034
+ returnData.push({ json: restored });
1035
+ }
1036
+ if (deleteAfterRestore) {
1037
+ vault.deleteSession(actualSessionId);
1038
+ }
1039
+ return [returnData];
1040
+ }
1041
+ // ─── PURGE (delete vault sessions) ──
1042
+ if (operation === 'purge') {
1043
+ const purgeScope = this.getNodeParameter('purgeScope', 0, 'all');
1044
+ if (purgeScope === 'specific') {
1045
+ const purgeSessionId = this.getNodeParameter('purgeSessionId', 0, '');
1046
+ if (!purgeSessionId) {
1047
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Session ID is required for purging a specific session.');
1048
+ }
1049
+ const session = vault.getSession(purgeSessionId);
1050
+ if (session) {
1051
+ const entryCount = Object.keys(session.entries).length;
1052
+ vault.deleteSession(purgeSessionId);
1053
+ return [[{
1054
+ json: {
1055
+ purged: true,
1056
+ sessionId: purgeSessionId,
1057
+ entriesDeleted: entryCount,
1058
+ message: `Session purged successfully.`,
1059
+ },
1060
+ }]];
1061
+ }
1062
+ else {
1063
+ return [[{
1064
+ json: {
1065
+ purged: false,
1066
+ sessionId: purgeSessionId,
1067
+ message: `No session found with this ID.`,
1068
+ },
1069
+ }]];
1070
+ }
1071
+ }
1072
+ else {
1073
+ // Purge ALL sessions
1074
+ const sessions = vault.listSessions();
1075
+ let totalEntries = 0;
1076
+ for (const s of sessions) {
1077
+ totalEntries += s.entryCount;
1078
+ vault.deleteSession(s.sessionId);
1079
+ }
1080
+ return [[{
1081
+ json: {
1082
+ purged: true,
1083
+ sessionsPurged: sessions.length,
1084
+ totalEntriesDeleted: totalEntries,
1085
+ message: `All ${sessions.length} session(s) purged successfully.`,
1086
+ },
1087
+ }]];
1088
+ }
1089
+ }
1090
+ return [[]];
1091
+ }
1092
+ }
1093
+ exports.PiiRedactor = PiiRedactor;