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.
- package/LICENSE +42 -0
- package/README.dev.md +153 -0
- package/README.md +443 -0
- package/README.npm.md +443 -0
- package/dist/nodes/PiiRedactor/PiiRedactor.node.d.ts +5 -0
- package/dist/nodes/PiiRedactor/PiiRedactor.node.js +1093 -0
- package/dist/nodes/PiiRedactor/__tests__/encryption.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/encryption.test.js +200 -0
- package/dist/nodes/PiiRedactor/__tests__/engine.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/engine.test.js +524 -0
- package/dist/nodes/PiiRedactor/__tests__/operations.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/operations.test.js +316 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns-global.test.js +427 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/patterns.test.js +481 -0
- package/dist/nodes/PiiRedactor/__tests__/phase1.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase1.test.js +343 -0
- package/dist/nodes/PiiRedactor/__tests__/phase3.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase3.test.js +275 -0
- package/dist/nodes/PiiRedactor/__tests__/phase4.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/phase4.test.js +184 -0
- package/dist/nodes/PiiRedactor/__tests__/presidio.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/presidio.test.js +170 -0
- package/dist/nodes/PiiRedactor/__tests__/security.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/security.test.js +178 -0
- package/dist/nodes/PiiRedactor/__tests__/semantic.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/semantic.test.js +319 -0
- package/dist/nodes/PiiRedactor/__tests__/vault.test.d.ts +1 -0
- package/dist/nodes/PiiRedactor/__tests__/vault.test.js +247 -0
- package/dist/nodes/PiiRedactor/audit.d.ts +48 -0
- package/dist/nodes/PiiRedactor/audit.js +192 -0
- package/dist/nodes/PiiRedactor/classification.d.ts +33 -0
- package/dist/nodes/PiiRedactor/classification.js +118 -0
- package/dist/nodes/PiiRedactor/context.d.ts +57 -0
- package/dist/nodes/PiiRedactor/context.js +260 -0
- package/dist/nodes/PiiRedactor/encryption.d.ts +45 -0
- package/dist/nodes/PiiRedactor/encryption.js +158 -0
- package/dist/nodes/PiiRedactor/engine.d.ts +23 -0
- package/dist/nodes/PiiRedactor/engine.js +888 -0
- package/dist/nodes/PiiRedactor/injection.d.ts +46 -0
- package/dist/nodes/PiiRedactor/injection.js +425 -0
- package/dist/nodes/PiiRedactor/names.d.ts +25 -0
- package/dist/nodes/PiiRedactor/names.js +188 -0
- package/dist/nodes/PiiRedactor/patterns.d.ts +17 -0
- package/dist/nodes/PiiRedactor/patterns.js +1742 -0
- package/dist/nodes/PiiRedactor/presidio.d.ts +77 -0
- package/dist/nodes/PiiRedactor/presidio.js +264 -0
- package/dist/nodes/PiiRedactor/profiles.d.ts +47 -0
- package/dist/nodes/PiiRedactor/profiles.js +139 -0
- package/dist/nodes/PiiRedactor/pseudonymize.d.ts +20 -0
- package/dist/nodes/PiiRedactor/pseudonymize.js +203 -0
- package/dist/nodes/PiiRedactor/redact.png +0 -0
- package/dist/nodes/PiiRedactor/redact.svg +3 -0
- package/dist/nodes/PiiRedactor/ropa.d.ts +63 -0
- package/dist/nodes/PiiRedactor/ropa.js +70 -0
- package/dist/nodes/PiiRedactor/types.d.ts +82 -0
- package/dist/nodes/PiiRedactor/types.js +3 -0
- package/dist/nodes/PiiRedactor/vault.d.ts +61 -0
- package/dist/nodes/PiiRedactor/vault.js +352 -0
- 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;
|