shroud-privacy 2.3.0 → 2.4.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.
@@ -167,8 +167,10 @@ export class ConfigManager {
167
167
  for (let i = 0; i < BUILTIN_PATTERNS.length; i++) {
168
168
  const p = BUILTIN_PATTERNS[i];
169
169
  const comma = i < BUILTIN_PATTERNS.length - 1 ? "," : "";
170
- // Convert RegExp to source string
171
- lines.push(` "${p.name}": { "pattern": ${JSON.stringify(p.pattern.source)}, "category": "${p.category}", "confidence": ${p.confidence} }${comma}`);
170
+ // Convert RegExp to source string + flags
171
+ const flags = p.pattern.flags.replace("g", "") || undefined; // "g" is always added; only store extra flags (i, m, etc.)
172
+ const flagsPart = flags ? `, "flags": "${flags}"` : "";
173
+ lines.push(` "${p.name}": { "pattern": ${JSON.stringify(p.pattern.source)}, "category": "${p.category}", "confidence": ${p.confidence}${flagsPart} }${comma}`);
172
174
  }
173
175
  lines.push(" }");
174
176
  lines.push("}");
@@ -23,6 +23,8 @@ export type DetectorOverrides = Record<string, {
23
23
  export type ConfigRule = {
24
24
  enabled?: boolean;
25
25
  pattern?: string;
26
+ /** Extra regex flags beyond "g" (e.g. "i", "m", "im"). Always gets "g" automatically. */
27
+ flags?: string;
26
28
  category?: string;
27
29
  confidence?: number;
28
30
  };
@@ -1179,6 +1179,251 @@ export const BUILTIN_PATTERNS = [
1179
1179
  category: Category.ACL_NAME,
1180
1180
  confidence: 0.85,
1181
1181
  },
1182
+ // ===================================================================
1183
+ // Healthcare — HIPAA-relevant identifiers
1184
+ // ===================================================================
1185
+ // --- Date of birth (context-triggered) ---
1186
+ {
1187
+ // "DOB: 03/15/1987" or "DOB 1987-03-15" or "date of birth: 03/15/1987"
1188
+ name: "dob_keyword",
1189
+ pattern: /(?:DOB|date\s+of\s+birth|birthdate|born\s+on|birth\s+date|geburtsdatum)\s*[:=]?\s*(\d{1,2}[\/\-.]\d{1,2}[\/\-.]\d{2,4})/gi,
1190
+ category: Category.DATE_OF_BIRTH,
1191
+ confidence: 0.9,
1192
+ },
1193
+ {
1194
+ // ISO date after DOB keyword: "DOB: 1987-03-15"
1195
+ name: "dob_iso",
1196
+ pattern: /(?:DOB|date\s+of\s+birth|birthdate|birth\s+date)\s*[:=]?\s*(\d{4}-\d{2}-\d{2})/gi,
1197
+ category: Category.DATE_OF_BIRTH,
1198
+ confidence: 0.9,
1199
+ },
1200
+ {
1201
+ // Written month: "born on March 15, 1987" or "DOB: January 3, 1990"
1202
+ name: "dob_written",
1203
+ pattern: /(?:DOB|date\s+of\s+birth|birthdate|born\s+on|birth\s+date)\s*[:=]?\s*((?:January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2},?\s+\d{4})/gi,
1204
+ category: Category.DATE_OF_BIRTH,
1205
+ confidence: 0.9,
1206
+ },
1207
+ // --- Medical record number (MRN) ---
1208
+ {
1209
+ // "MRN: 12345678" or "medical record number: ABC-123456"
1210
+ name: "mrn_keyword",
1211
+ pattern: /(?:MRN|medical\s*record\s*(?:number|no|#)?|patient\s*(?:id|number|#))\s*[:=#]?\s*([A-Z0-9][\w\-]{3,19})/gi,
1212
+ category: Category.MEDICAL_RECORD_NUMBER,
1213
+ confidence: 0.9,
1214
+ },
1215
+ {
1216
+ // US NPI (National Provider Identifier): 10-digit number with "NPI" context
1217
+ name: "npi_number",
1218
+ pattern: /(?:NPI|national\s*provider)\s*[:=#]?\s*(\d{10})\b/gi,
1219
+ category: Category.MEDICAL_RECORD_NUMBER,
1220
+ confidence: 0.95,
1221
+ },
1222
+ {
1223
+ // US DEA number: 2 letters + 7 digits (e.g., "AB1234567")
1224
+ name: "dea_number",
1225
+ pattern: /(?:DEA|drug\s*enforcement)\s*[:=#]?\s*([A-Z][A-Z9]\d{7})\b/gi,
1226
+ category: Category.MEDICAL_RECORD_NUMBER,
1227
+ confidence: 0.95,
1228
+ },
1229
+ {
1230
+ // Health insurance policy/member/subscriber ID
1231
+ name: "health_insurance_id",
1232
+ pattern: /(?:(?:health\s*)?(?:insurance|policy|member|subscriber|group)\s*(?:id|number|no|#))\s*[:=#]?\s*([A-Z0-9][\w\-]{3,19})/gi,
1233
+ category: Category.MEDICAL_RECORD_NUMBER,
1234
+ confidence: 0.85,
1235
+ },
1236
+ // ===================================================================
1237
+ // Finance — bank accounts, routing numbers, tax IDs
1238
+ // ===================================================================
1239
+ // --- Bank account numbers (context-triggered) ---
1240
+ {
1241
+ // US routing number (ABA/transit): 9 digits with context
1242
+ name: "us_routing_number",
1243
+ pattern: /(?:routing|ABA|transit)\s*(?:number|no|#)?\s*[:=#]?\s*(\d{9})\b/gi,
1244
+ category: Category.BANK_ACCOUNT_NUMBER,
1245
+ confidence: 0.9,
1246
+ },
1247
+ {
1248
+ // Bank account number with context keyword
1249
+ name: "bank_account_keyword",
1250
+ pattern: /(?:(?:bank\s*)?account|acct)\s*(?:number|no|#)\s*[:=#]?\s*(\d{4,17})\b/gi,
1251
+ category: Category.BANK_ACCOUNT_NUMBER,
1252
+ confidence: 0.85,
1253
+ },
1254
+ {
1255
+ // UK sort code: XX-XX-XX with context
1256
+ name: "uk_sort_code",
1257
+ pattern: /(?:sort\s*code)\s*[:=#]?\s*(\d{2}-\d{2}-\d{2})\b/gi,
1258
+ category: Category.BANK_ACCOUNT_NUMBER,
1259
+ confidence: 0.9,
1260
+ },
1261
+ {
1262
+ // SWIFT/BIC code: 8 or 11 alphanumeric characters (e.g., DEUTDEFF, BOFAUS3NXXX)
1263
+ name: "swift_bic",
1264
+ pattern: /(?:SWIFT|BIC|SWIFT\/BIC)\s*[:=#]?\s*([A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\b/gi,
1265
+ category: Category.BANK_ACCOUNT_NUMBER,
1266
+ confidence: 0.9,
1267
+ },
1268
+ {
1269
+ // Standalone SWIFT/BIC pattern (distinctive 8/11-char format)
1270
+ name: "swift_bic_standalone",
1271
+ pattern: /\b([A-Z]{4}(?:AT|DE|CH|GB|US|FR|IT|ES|NL|BE|AU|CA|JP|CN|IN|BR|MX|ZA|SE|NO|DK|FI|IE|PT|CZ|PL|HU|RO|BG|HR|SK|SI|LT|LV|EE|MT|CY|LU|GR)[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\b/g,
1272
+ category: Category.BANK_ACCOUNT_NUMBER,
1273
+ confidence: 0.85,
1274
+ },
1275
+ // --- Tax IDs ---
1276
+ {
1277
+ // US EIN (Employer Identification Number): XX-XXXXXXX with context
1278
+ name: "us_ein",
1279
+ pattern: /(?:EIN|employer\s*(?:identification|id)\s*(?:number|no|#)?|tax\s*(?:id|identification)\s*(?:number|no|#)?)\s*[:=#]?\s*(\d{2}-\d{7})\b/gi,
1280
+ category: Category.TAX_ID,
1281
+ confidence: 0.9,
1282
+ },
1283
+ {
1284
+ // TIN/tax number generic context-triggered
1285
+ name: "tax_id_generic",
1286
+ pattern: /(?:TIN|tax\s*(?:id|number|identification)|taxpayer\s*(?:id|number))\s*[:=#]?\s*(\d[\d\-]{5,12})\b/gi,
1287
+ category: Category.TAX_ID,
1288
+ confidence: 0.85,
1289
+ },
1290
+ {
1291
+ // UK UTR (Unique Taxpayer Reference): 10 digits with context
1292
+ name: "uk_utr",
1293
+ pattern: /(?:UTR|unique\s*taxpayer\s*(?:reference|ref))\s*[:=#]?\s*(\d{10})\b/gi,
1294
+ category: Category.TAX_ID,
1295
+ confidence: 0.9,
1296
+ },
1297
+ // ===================================================================
1298
+ // Legal / Identity Documents
1299
+ // ===================================================================
1300
+ // --- Passport numbers (context-triggered) ---
1301
+ {
1302
+ // Generic context: "passport no: P12345678" or "passport number: 123456789"
1303
+ name: "passport_keyword",
1304
+ pattern: /(?:passport)\s*(?:number|no|#)?\s*[:=#]?\s*([A-Z0-9]{6,12})\b/gi,
1305
+ category: Category.PASSPORT_NUMBER,
1306
+ confidence: 0.9,
1307
+ },
1308
+ {
1309
+ // German Reisepass: "Reisepass: C01X00T47"
1310
+ name: "passport_de",
1311
+ pattern: /(?:Reisepass|Personalausweis)\s*(?:Nr|number|no|#)?\.?\s*[:=#]?\s*([CFGHJKLMNPRTVWXYZ0-9]{9})\b/gi,
1312
+ category: Category.PASSPORT_NUMBER,
1313
+ confidence: 0.9,
1314
+ },
1315
+ {
1316
+ // Travel document context: "travel document: XX1234567"
1317
+ name: "travel_document",
1318
+ pattern: /(?:travel\s*document)\s*(?:number|no|#)?\s*[:=#]?\s*([A-Z0-9]{6,12})\b/gi,
1319
+ category: Category.PASSPORT_NUMBER,
1320
+ confidence: 0.85,
1321
+ },
1322
+ // --- Driver's license (context-triggered) ---
1323
+ {
1324
+ // Generic: "driver's license: A1234567" or "DL: 12345678"
1325
+ name: "drivers_license_keyword",
1326
+ pattern: /(?:driver'?s?\s*licen[sc]e|DL|driving\s*licen[sc]e|F[üu]hrerschein)\s*(?:number|no|#|Nr)?\.?\s*[:=#]?\s*([A-Z0-9][\w\-]{3,19})\b/gi,
1327
+ category: Category.DRIVERS_LICENSE,
1328
+ confidence: 0.9,
1329
+ },
1330
+ {
1331
+ // US California format: letter + 7 digits with context
1332
+ name: "dl_california",
1333
+ pattern: /(?:driver'?s?\s*licen[sc]e|DL)\s*(?:number|no|#)?\s*[:=#]?\s*([A-Z]\d{7})\b/gi,
1334
+ category: Category.DRIVERS_LICENSE,
1335
+ confidence: 0.9,
1336
+ },
1337
+ {
1338
+ // License plate / vehicle registration (context-triggered, requires : or = separator)
1339
+ name: "license_plate",
1340
+ pattern: /(?:licen[sc]e\s*plate|registration\s*(?:number|no|#|plate)|vehicle\s*(?:plate|reg|registration)|Kennzeichen|plaque)\s*[:=#]\s*([A-Z][A-Z0-9\-]{1,11}[A-Z0-9])\b/gi,
1341
+ category: Category.DRIVERS_LICENSE,
1342
+ confidence: 0.85,
1343
+ },
1344
+ // --- Court case / docket numbers ---
1345
+ {
1346
+ // US federal court: "1:23-cv-01234" or "2:24-cr-00567"
1347
+ name: "us_federal_case",
1348
+ pattern: /\b(\d{1,2}:\d{2}-[a-z]{2}-\d{3,6})\b/g,
1349
+ category: Category.CASE_NUMBER,
1350
+ confidence: 0.95,
1351
+ },
1352
+ {
1353
+ // Generic case/docket number with context keyword
1354
+ name: "case_number_keyword",
1355
+ pattern: /(?:case|docket|cause|filing)\s*(?:number|no|#)?\s*[:=#]?\s*([A-Z0-9][\w\-\/]{3,24})\b/gi,
1356
+ category: Category.CASE_NUMBER,
1357
+ confidence: 0.85,
1358
+ },
1359
+ {
1360
+ // Patent number: "US12345678" or "EP1234567" or "WO2024123456"
1361
+ name: "patent_number",
1362
+ pattern: /\b((?:US|EP|WO|JP|CN|KR|AU|CA)\s?\d{4,12}(?:\s?[AB]\d?)?)\b/g,
1363
+ category: Category.CASE_NUMBER,
1364
+ confidence: 0.85,
1365
+ },
1366
+ {
1367
+ // Aktenzeichen (German file reference): "Az.: 1 BvR 123/45" or "Az. 12 O 456/23"
1368
+ name: "aktenzeichen",
1369
+ pattern: /(?:Az\.?|Aktenzeichen)\s*[:=]?\s*(\d{1,3}\s+[A-Z][a-z]*\s+\d{1,5}\/\d{2,4})\b/gi,
1370
+ category: Category.CASE_NUMBER,
1371
+ confidence: 0.9,
1372
+ },
1373
+ // ===================================================================
1374
+ // Cloud / Crypto
1375
+ // ===================================================================
1376
+ // --- Cryptocurrency addresses (standalone — distinctive formats) ---
1377
+ {
1378
+ // Ethereum address: 0x + 40 hex chars
1379
+ name: "crypto_ethereum",
1380
+ pattern: /\b(0x[0-9a-fA-F]{40})\b/g,
1381
+ category: Category.CRYPTOCURRENCY_ADDRESS,
1382
+ confidence: 0.95,
1383
+ },
1384
+ {
1385
+ // Bitcoin P2PKH: starts with 1, 25-34 base58 chars
1386
+ name: "crypto_bitcoin_p2pkh",
1387
+ pattern: /\b(1[a-km-zA-HJ-NP-Z1-9]{25,34})\b/g,
1388
+ category: Category.CRYPTOCURRENCY_ADDRESS,
1389
+ confidence: 0.9,
1390
+ },
1391
+ {
1392
+ // Bitcoin P2SH: starts with 3, 25-34 base58 chars
1393
+ name: "crypto_bitcoin_p2sh",
1394
+ pattern: /\b(3[a-km-zA-HJ-NP-Z1-9]{25,34})\b/g,
1395
+ category: Category.CRYPTOCURRENCY_ADDRESS,
1396
+ confidence: 0.9,
1397
+ },
1398
+ {
1399
+ // Bitcoin Bech32: bc1 + 39-59 lowercase alphanum
1400
+ name: "crypto_bitcoin_bech32",
1401
+ pattern: /\b(bc1[a-z0-9]{39,59})\b/g,
1402
+ category: Category.CRYPTOCURRENCY_ADDRESS,
1403
+ confidence: 0.95,
1404
+ },
1405
+ {
1406
+ // Crypto wallet/address with context keyword
1407
+ name: "crypto_wallet_keyword",
1408
+ pattern: /(?:wallet|crypto|bitcoin|ethereum|eth|btc)\s*(?:address|addr|id)?\s*[:=#]?\s*([a-zA-Z0-9]{26,64})\b/gi,
1409
+ category: Category.CRYPTOCURRENCY_ADDRESS,
1410
+ confidence: 0.85,
1411
+ },
1412
+ // --- AWS ARN (standalone — distinctive format) ---
1413
+ {
1414
+ // Full ARN: arn:aws:service:region:account-id:resource (account may be empty for S3)
1415
+ name: "aws_arn",
1416
+ pattern: /\b(arn:aws[a-z\-]*:[a-z0-9\-]+:[a-z0-9\-]*:\d{0,12}:[^\s"']{1,128})\b/g,
1417
+ category: Category.AWS_ARN,
1418
+ confidence: 0.95,
1419
+ },
1420
+ {
1421
+ // AWS account ID with context: 12-digit number after "account" keyword
1422
+ name: "aws_account_id",
1423
+ pattern: /(?:(?:aws|amazon)\s*)?account\s*(?:id|#|number)?\s*[:=#]?\s*(\d{12})\b/gi,
1424
+ category: Category.AWS_ARN,
1425
+ confidence: 0.85,
1426
+ },
1182
1427
  ];
1183
1428
  /**
1184
1429
  * Tracks occupied text spans and answers overlap queries in O(log n)
@@ -1273,8 +1518,9 @@ export class RegexDetector {
1273
1518
  return p;
1274
1519
  const updated = { ...p };
1275
1520
  if (rule.pattern) {
1521
+ const flags = "g" + (rule.flags || "").replace(/g/g, "");
1276
1522
  try {
1277
- updated.pattern = new RegExp(rule.pattern, "g");
1523
+ updated.pattern = new RegExp(rule.pattern, flags);
1278
1524
  }
1279
1525
  catch { /* invalid regex — keep built-in */ }
1280
1526
  }
@@ -1298,9 +1544,10 @@ export class RegexDetector {
1298
1544
  if (!rule.pattern)
1299
1545
  continue; // new rules must have a pattern
1300
1546
  try {
1547
+ const flags = "g" + (rule.flags || "").replace(/g/g, "");
1301
1548
  patterns.push({
1302
1549
  name,
1303
- pattern: new RegExp(rule.pattern, "g"),
1550
+ pattern: new RegExp(rule.pattern, flags),
1304
1551
  category: rule.category ? resolveCategory(rule.category) : Category.CUSTOM,
1305
1552
  confidence: rule.confidence ?? 0.9,
1306
1553
  });
@@ -14,7 +14,72 @@ export declare class CodeGenerator implements BaseGenerator {
14
14
  _fakeSsn(seed: number): string;
15
15
  _fakePhone(seed: number, original: string): string;
16
16
  _fakeIban(seed: number, original: string): string;
17
+ /**
18
+ * Format-aware national ID generator.
19
+ * Preserves structure per detected sub-type rather than generic zero-padding.
20
+ */
17
21
  _fakeNationalId(seed: number, original: string): string;
18
22
  _fakeJwt(seed: number): string;
23
+ /**
24
+ * GPS coordinate generator — distributes across plausible world locations
25
+ * instead of clustering near null island (0,0).
26
+ */
19
27
  _fakeGps(seed: number): string;
28
+ /**
29
+ * ICS/SCADA identifier — format-aware per sub-type.
30
+ * Preserves structure for OPC UA endpoints, Modbus addresses, BACnet IDs, etc.
31
+ */
32
+ _fakeIcsId(seed: number, original: string): string;
33
+ /**
34
+ * Certificate generator — produces structurally valid PEM-like blocks
35
+ * instead of [REDACTED-CERT-XXXX] placeholders.
36
+ */
37
+ _fakeCertificate(seed: number, original: string): string;
38
+ /**
39
+ * Format-preserving fake date of birth.
40
+ * Shifts the date by a deterministic offset (30-300 days) derived from seed,
41
+ * preserving the original format (MM/DD/YYYY, YYYY-MM-DD, DD.MM.YYYY, written month).
42
+ */
43
+ _fakeDob(seed: number, original: string): string;
44
+ /**
45
+ * Fake medical record / provider ID.
46
+ * Preserves format structure (letter+digits, pure digits, with dashes).
47
+ */
48
+ _fakeMedicalId(seed: number, original: string): string;
49
+ /**
50
+ * Fake bank account / routing number.
51
+ * Preserves format (digit count, dashes, sort code format).
52
+ */
53
+ _fakeBankAccount(seed: number, original: string): string;
54
+ /**
55
+ * Fake tax ID / EIN.
56
+ * Preserves format (XX-XXXXXXX for EIN, pure digits for others).
57
+ */
58
+ _fakeTaxId(seed: number, original: string): string;
59
+ /**
60
+ * Fake passport number.
61
+ * Preserves format: letter prefix + digit count, or pure alphanumeric.
62
+ */
63
+ _fakePassport(seed: number, original: string): string;
64
+ /**
65
+ * Fake driver's license / license plate.
66
+ * Preserves format structure (letter/digit positions, dashes, spaces).
67
+ */
68
+ _fakeDriversLicense(seed: number, original: string): string;
69
+ /**
70
+ * Fake case / docket / patent number.
71
+ * Preserves format: digit:digit-letters-digits, or prefix + digits.
72
+ */
73
+ _fakeCaseNumber(seed: number, original: string): string;
74
+ /**
75
+ * Fake cryptocurrency address.
76
+ * Preserves format: Ethereum (0x + 40 hex), Bitcoin P2PKH/P2SH (base58),
77
+ * Bitcoin Bech32 (bc1 + lowercase alphanum).
78
+ */
79
+ _fakeCryptoAddress(seed: number, original: string): string;
80
+ /**
81
+ * Fake AWS ARN.
82
+ * Preserves service and resource type, replaces account ID and resource name.
83
+ */
84
+ _fakeAwsArn(seed: number, original: string): string;
20
85
  }
@@ -42,6 +42,15 @@ export class CodeGenerator {
42
42
  Category.GPS_COORDINATE,
43
43
  Category.ICS_IDENTIFIER,
44
44
  Category.CERTIFICATE,
45
+ Category.DATE_OF_BIRTH,
46
+ Category.MEDICAL_RECORD_NUMBER,
47
+ Category.BANK_ACCOUNT_NUMBER,
48
+ Category.TAX_ID,
49
+ Category.PASSPORT_NUMBER,
50
+ Category.DRIVERS_LICENSE,
51
+ Category.CASE_NUMBER,
52
+ Category.CRYPTOCURRENCY_ADDRESS,
53
+ Category.AWS_ARN,
45
54
  ];
46
55
  generate(category, seed, original = "") {
47
56
  if (category === Category.API_KEY) {
@@ -72,10 +81,37 @@ export class CodeGenerator {
72
81
  return this._fakeGps(seed);
73
82
  }
74
83
  else if (category === Category.ICS_IDENTIFIER) {
75
- return `ICS-${String(seed % 100000).padStart(5, "0")}`;
84
+ return this._fakeIcsId(seed, original);
76
85
  }
77
86
  else if (category === Category.CERTIFICATE) {
78
- return `[REDACTED-CERT-${String(seed % 10000).padStart(4, "0")}]`;
87
+ return this._fakeCertificate(seed, original);
88
+ }
89
+ else if (category === Category.DATE_OF_BIRTH) {
90
+ return this._fakeDob(seed, original);
91
+ }
92
+ else if (category === Category.MEDICAL_RECORD_NUMBER) {
93
+ return this._fakeMedicalId(seed, original);
94
+ }
95
+ else if (category === Category.BANK_ACCOUNT_NUMBER) {
96
+ return this._fakeBankAccount(seed, original);
97
+ }
98
+ else if (category === Category.TAX_ID) {
99
+ return this._fakeTaxId(seed, original);
100
+ }
101
+ else if (category === Category.PASSPORT_NUMBER) {
102
+ return this._fakePassport(seed, original);
103
+ }
104
+ else if (category === Category.DRIVERS_LICENSE) {
105
+ return this._fakeDriversLicense(seed, original);
106
+ }
107
+ else if (category === Category.CASE_NUMBER) {
108
+ return this._fakeCaseNumber(seed, original);
109
+ }
110
+ else if (category === Category.CRYPTOCURRENCY_ADDRESS) {
111
+ return this._fakeCryptoAddress(seed, original);
112
+ }
113
+ else if (category === Category.AWS_ARN) {
114
+ return this._fakeAwsArn(seed, original);
79
115
  }
80
116
  return `code-${String(seed % 10000).padStart(4, "0")}`;
81
117
  }
@@ -212,20 +248,411 @@ export class CodeGenerator {
212
248
  const body = String(seed).padStart(18, "0").slice(0, 18);
213
249
  return `${cc}${check}${body}`;
214
250
  }
251
+ /**
252
+ * Format-aware national ID generator.
253
+ * Preserves structure per detected sub-type rather than generic zero-padding.
254
+ */
215
255
  _fakeNationalId(seed, original) {
216
- // Preserve length and format (digits/letters)
256
+ const h = createHash("sha256").update(`natid:${seed}`).digest("hex");
217
257
  const len = original.length || 10;
218
- const digits = String(seed).padStart(len, "0").slice(0, len);
219
- return digits;
258
+ // Austrian SVNR: 4 digits + DDMMYY (e.g., "1234 010190")
259
+ if (/^\d{4}\s?\d{6}$/.test(original)) {
260
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
261
+ const hasSpace = original.includes(" ");
262
+ return hasSpace
263
+ ? `${d.slice(0, 4)} ${d.slice(4, 10)}`
264
+ : d.slice(0, 10);
265
+ }
266
+ // German Personalausweis: alphanumeric 9-10 chars with specific char set
267
+ if (/^[CFGHJKLMNPRTVWXYZ0-9]{9,10}$/.test(original)) {
268
+ const charset = "CFGHJKLMNPRTVWXYZ0123456789";
269
+ let result = "";
270
+ for (let i = 0; i < len; i++) {
271
+ result += charset[parseInt(h[i * 2] + h[i * 2 + 1], 16) % charset.length];
272
+ }
273
+ return result;
274
+ }
275
+ // Generic: preserve format character-by-character (letters stay letters, digits stay digits)
276
+ let result = "";
277
+ for (let i = 0; i < len; i++) {
278
+ const c = original[i];
279
+ if (!c || /[0-9]/.test(c)) {
280
+ result += String(parseInt(h[i] || "0", 16) % 10);
281
+ }
282
+ else if (/[A-Z]/.test(c)) {
283
+ result += "ABCDEFGHJKLMNPRSTUVWXYZ"[parseInt(h[i * 2] || "0", 16) % 23];
284
+ }
285
+ else if (/[a-z]/.test(c)) {
286
+ result += "abcdefghjklmnprstuvwxyz"[parseInt(h[i * 2] || "0", 16) % 23];
287
+ }
288
+ else {
289
+ result += c; // preserve dashes, spaces, etc.
290
+ }
291
+ }
292
+ return result;
220
293
  }
221
294
  _fakeJwt(seed) {
222
295
  const h = createHash("sha256").update(String(seed)).digest("base64url");
223
296
  return `eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.${h}`;
224
297
  }
298
+ /**
299
+ * GPS coordinate generator — distributes across plausible world locations
300
+ * instead of clustering near null island (0,0).
301
+ */
225
302
  _fakeGps(seed) {
226
- // Generate coordinates near null island (0,0) — clearly fake
227
- const lat = ((seed % 18000) / 100 - 90).toFixed(6);
228
- const lon = (((seed >>> 8) % 36000) / 100 - 180).toFixed(6);
303
+ // Anchor points across continents for realistic-looking coordinates
304
+ const anchors = [
305
+ [48.2, 16.4], // Vienna area
306
+ [40.7, -74.0], // New York area
307
+ [51.5, -0.1], // London area
308
+ [35.7, 139.7], // Tokyo area
309
+ [-33.9, 151.2], // Sydney area
310
+ [55.8, 37.6], // Moscow area
311
+ [-23.5, -46.6], // São Paulo area
312
+ [37.8, -122.4], // San Francisco area
313
+ [52.5, 13.4], // Berlin area
314
+ [1.3, 103.8], // Singapore area
315
+ ];
316
+ const anchor = anchors[seed % anchors.length];
317
+ // Add deterministic offset ±2 degrees (stays in the general area)
318
+ const latOffset = ((seed >>> 4) % 400 - 200) / 100;
319
+ const lonOffset = ((seed >>> 12) % 400 - 200) / 100;
320
+ const lat = (anchor[0] + latOffset).toFixed(6);
321
+ const lon = (anchor[1] + lonOffset).toFixed(6);
229
322
  return `${lat}, ${lon}`;
230
323
  }
324
+ /**
325
+ * ICS/SCADA identifier — format-aware per sub-type.
326
+ * Preserves structure for OPC UA endpoints, Modbus addresses, BACnet IDs, etc.
327
+ */
328
+ _fakeIcsId(seed, original) {
329
+ const h = createHash("sha256").update(`ics:${seed}`).digest("hex");
330
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
331
+ // OPC UA endpoint: "opc.tcp://host:port/path"
332
+ if (/^opc\.tcp:\/\//i.test(original)) {
333
+ return `opc.tcp://plc-${d.slice(0, 4)}.local:${4840 + (seed % 100)}/${h.slice(0, 8)}`;
334
+ }
335
+ // Modbus/slave/unit address: small number
336
+ if (/^\d{1,3}$/.test(original)) {
337
+ return String((seed % 247) + 1); // Modbus range 1-247
338
+ }
339
+ // BACnet device instance: up to 7 digits
340
+ if (/^\d{1,7}$/.test(original) && original.length > 3) {
341
+ return d.slice(0, original.length);
342
+ }
343
+ // DNP3 outstation address: up to 5 digits
344
+ if (/^\d{1,5}$/.test(original)) {
345
+ return d.slice(0, original.length);
346
+ }
347
+ // IEC 61850 IED name: alphanumeric device name
348
+ if (/^[A-Z][A-Za-z0-9_\-]{2,}$/.test(original)) {
349
+ const prefixes = ["PLC", "RTU", "IED", "HMI", "DCS"];
350
+ return prefixes[seed % prefixes.length] + "-" + d.slice(0, 4);
351
+ }
352
+ // Historian tag: dotted path like "Plant.Area.Tag"
353
+ if (original.includes(".") && /^[A-Za-z]/.test(original)) {
354
+ const parts = original.split(".");
355
+ const fakeParts = parts.map((_, i) => {
356
+ const labels = i === 0 ? ["Plant", "Site", "Facility"]
357
+ : i === parts.length - 1 ? ["Tag", "Point", "Value", "Status"]
358
+ : ["Area", "Unit", "Zone", "Line"];
359
+ return labels[parseInt(h[i * 2] || "0", 16) % labels.length] + d.slice(i, i + 2);
360
+ });
361
+ return fakeParts.join(".");
362
+ }
363
+ // Fallback: prefix + digits
364
+ return `ICS-${d.slice(0, 5)}`;
365
+ }
366
+ /**
367
+ * Certificate generator — produces structurally valid PEM-like blocks
368
+ * instead of [REDACTED-CERT-XXXX] placeholders.
369
+ */
370
+ _fakeCertificate(seed, original) {
371
+ const h = createHash("sha256").update(`cert:${seed}`).digest("hex");
372
+ // PEM private key
373
+ if (/BEGIN.*PRIVATE KEY/i.test(original)) {
374
+ const fakeKey = Buffer.from(h + h + h + h).toString("base64");
375
+ const lines = [];
376
+ for (let i = 0; i < fakeKey.length; i += 64) {
377
+ lines.push(fakeKey.slice(i, i + 64));
378
+ }
379
+ return `-----BEGIN PRIVATE KEY-----\n${lines.join("\n")}\n-----END PRIVATE KEY-----`;
380
+ }
381
+ // PEM certificate
382
+ if (/BEGIN CERTIFICATE/i.test(original)) {
383
+ const fakeCert = Buffer.from(h + h + h + h + h + h).toString("base64");
384
+ const lines = [];
385
+ for (let i = 0; i < fakeCert.length; i += 64) {
386
+ lines.push(fakeCert.slice(i, i + 64));
387
+ }
388
+ return `-----BEGIN CERTIFICATE-----\n${lines.join("\n")}\n-----END CERTIFICATE-----`;
389
+ }
390
+ // Short cert/key reference
391
+ return `CERT-${h.slice(0, 12).toUpperCase()}`;
392
+ }
393
+ /**
394
+ * Format-preserving fake date of birth.
395
+ * Shifts the date by a deterministic offset (30-300 days) derived from seed,
396
+ * preserving the original format (MM/DD/YYYY, YYYY-MM-DD, DD.MM.YYYY, written month).
397
+ */
398
+ _fakeDob(seed, original) {
399
+ const offset = (seed % 270) + 30; // 30-300 day shift
400
+ // Try to parse and reformat
401
+ // ISO: 1987-03-15
402
+ const isoMatch = original.match(/(\d{4})-(\d{2})-(\d{2})/);
403
+ if (isoMatch) {
404
+ const d = new Date(+isoMatch[1], +isoMatch[2] - 1, +isoMatch[3]);
405
+ d.setDate(d.getDate() + offset);
406
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
407
+ }
408
+ // US: MM/DD/YYYY or MM-DD-YYYY or MM.DD.YYYY
409
+ const usMatch = original.match(/(\d{1,2})([\/\-\.])(\d{1,2})\2(\d{2,4})/);
410
+ if (usMatch) {
411
+ const year = usMatch[4].length === 2 ? 1900 + +usMatch[4] : +usMatch[4];
412
+ const d = new Date(year, +usMatch[1] - 1, +usMatch[3]);
413
+ d.setDate(d.getDate() + offset);
414
+ const yStr = usMatch[4].length === 2 ? String(d.getFullYear()).slice(2) : String(d.getFullYear());
415
+ return `${String(d.getMonth() + 1).padStart(usMatch[1].length, "0")}${usMatch[2]}${String(d.getDate()).padStart(usMatch[3].length, "0")}${usMatch[2]}${yStr}`;
416
+ }
417
+ // Written month: "March 15, 1987"
418
+ const months = ["January", "February", "March", "April", "May", "June",
419
+ "July", "August", "September", "October", "November", "December"];
420
+ const writtenMatch = original.match(/(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})/i);
421
+ if (writtenMatch) {
422
+ const mi = months.findIndex(m => m.toLowerCase() === writtenMatch[1].toLowerCase());
423
+ const d = new Date(+writtenMatch[3], mi, +writtenMatch[2]);
424
+ d.setDate(d.getDate() + offset);
425
+ return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
426
+ }
427
+ // Fallback: shift digits
428
+ const shifted = String((seed % 28) + 1).padStart(2, "0") + "/" +
429
+ String((seed % 12) + 1).padStart(2, "0") + "/" +
430
+ String(1950 + (seed % 60));
431
+ return shifted;
432
+ }
433
+ /**
434
+ * Fake medical record / provider ID.
435
+ * Preserves format structure (letter+digits, pure digits, with dashes).
436
+ */
437
+ _fakeMedicalId(seed, original) {
438
+ const h = createHash("sha256").update(`mrn:${seed}`).digest("hex");
439
+ // DEA format: 2 letters + 7 digits
440
+ if (/^[A-Z][A-Z9]\d{7}$/.test(original)) {
441
+ const letters = "ABCDEFGHJKLMNPRSTUVWXYZ";
442
+ return letters[seed % letters.length] + letters[(seed >>> 4) % letters.length] +
443
+ h.replace(/[a-f]/g, d => String((parseInt(d, 16) % 10))).slice(0, 7);
444
+ }
445
+ // NPI: 10 digits
446
+ if (/^\d{10}$/.test(original)) {
447
+ return h.replace(/[a-f]/g, d => String((parseInt(d, 16) % 10))).slice(0, 10);
448
+ }
449
+ // Generic: preserve length, replace with alphanumeric
450
+ const len = original.length || 8;
451
+ let result = "";
452
+ for (let i = 0; i < len; i++) {
453
+ const c = original[i];
454
+ if (!c || /[A-Z]/.test(c)) {
455
+ result += "ABCDEFGHJKLMNPRSTUVWXYZ"[parseInt(h[i * 2] || "0", 16) % 23];
456
+ }
457
+ else if (/[0-9]/.test(c)) {
458
+ result += h[i] ? String(parseInt(h[i], 16) % 10) : "0";
459
+ }
460
+ else {
461
+ result += c; // preserve dashes, etc.
462
+ }
463
+ }
464
+ return result;
465
+ }
466
+ /**
467
+ * Fake bank account / routing number.
468
+ * Preserves format (digit count, dashes, sort code format).
469
+ */
470
+ _fakeBankAccount(seed, original) {
471
+ const h = createHash("sha256").update(`bank:${seed}`).digest("hex");
472
+ // SWIFT/BIC: 8 or 11 alphanumeric
473
+ if (/^[A-Z]{6}[A-Z0-9]{2}(?:[A-Z0-9]{3})?$/.test(original)) {
474
+ const banks = ["SHROUD", "FAKEBK", "TESTBK", "DEMOFI", "SYNBNK"];
475
+ const bank = banks[seed % banks.length];
476
+ const country = original.slice(4, 6); // preserve country code
477
+ const suffix = h.slice(0, 2).toUpperCase();
478
+ const branch = original.length === 11 ? h.slice(2, 5).toUpperCase() : "";
479
+ return bank + country + suffix + branch;
480
+ }
481
+ // Sort code: XX-XX-XX
482
+ if (/^\d{2}-\d{2}-\d{2}$/.test(original)) {
483
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
484
+ return `${d.slice(0, 2)}-${d.slice(2, 4)}-${d.slice(4, 6)}`;
485
+ }
486
+ // Digit-only: preserve length
487
+ const len = original.replace(/\D/g, "").length || 9;
488
+ const digits = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10)).slice(0, len);
489
+ return digits.padStart(len, "0");
490
+ }
491
+ /**
492
+ * Fake tax ID / EIN.
493
+ * Preserves format (XX-XXXXXXX for EIN, pure digits for others).
494
+ */
495
+ _fakeTaxId(seed, original) {
496
+ const h = createHash("sha256").update(`tax:${seed}`).digest("hex");
497
+ const digits = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
498
+ // EIN format: XX-XXXXXXX
499
+ if (/^\d{2}-\d{7}$/.test(original)) {
500
+ return `${digits.slice(0, 2)}-${digits.slice(2, 9)}`;
501
+ }
502
+ // Pure digits: preserve length
503
+ const len = original.replace(/\D/g, "").length || 10;
504
+ return digits.slice(0, len).padStart(len, "0");
505
+ }
506
+ /**
507
+ * Fake passport number.
508
+ * Preserves format: letter prefix + digit count, or pure alphanumeric.
509
+ */
510
+ _fakePassport(seed, original) {
511
+ const h = createHash("sha256").update(`passport:${seed}`).digest("hex");
512
+ const len = original.length || 9;
513
+ let result = "";
514
+ for (let i = 0; i < len; i++) {
515
+ const c = original[i];
516
+ if (!c || /[A-Z]/.test(c)) {
517
+ result += "ABCDEFGHJKLMNPRSTUVWXYZ"[parseInt(h[i * 2] || "0", 16) % 23];
518
+ }
519
+ else if (/[a-z]/.test(c)) {
520
+ result += "abcdefghjklmnprstuvwxyz"[parseInt(h[i * 2] || "0", 16) % 23];
521
+ }
522
+ else if (/[0-9]/.test(c)) {
523
+ result += String(parseInt(h[i] || "0", 16) % 10);
524
+ }
525
+ else {
526
+ result += c; // preserve spaces, dashes
527
+ }
528
+ }
529
+ return result;
530
+ }
531
+ /**
532
+ * Fake driver's license / license plate.
533
+ * Preserves format structure (letter/digit positions, dashes, spaces).
534
+ */
535
+ _fakeDriversLicense(seed, original) {
536
+ const h = createHash("sha256").update(`dl:${seed}`).digest("hex");
537
+ const len = original.length || 8;
538
+ let result = "";
539
+ for (let i = 0; i < len; i++) {
540
+ const c = original[i];
541
+ if (!c || /[A-Z]/.test(c)) {
542
+ result += "ABCDEFGHJKLMNPRSTUVWXYZ"[parseInt(h[i * 2] || "0", 16) % 23];
543
+ }
544
+ else if (/[0-9]/.test(c)) {
545
+ result += String(parseInt(h[i] || "0", 16) % 10);
546
+ }
547
+ else {
548
+ result += c; // preserve dashes, spaces
549
+ }
550
+ }
551
+ return result;
552
+ }
553
+ /**
554
+ * Fake case / docket / patent number.
555
+ * Preserves format: digit:digit-letters-digits, or prefix + digits.
556
+ */
557
+ _fakeCaseNumber(seed, original) {
558
+ const h = createHash("sha256").update(`case:${seed}`).digest("hex");
559
+ // US federal: X:XX-xx-XXXXX
560
+ const fedMatch = original.match(/^(\d{1,2}):(\d{2})-([a-z]{2})-(\d{3,6})$/);
561
+ if (fedMatch) {
562
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
563
+ return `${d.slice(0, fedMatch[1].length)}:${d.slice(2, 4)}-${fedMatch[3]}-${d.slice(4, 4 + fedMatch[4].length)}`;
564
+ }
565
+ // Patent: preserve country prefix + fake digits
566
+ const patentMatch = original.match(/^([A-Z]{2})\s?(\d+)(.*)$/);
567
+ if (patentMatch) {
568
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
569
+ return `${patentMatch[1]}${d.slice(0, patentMatch[2].length)}${patentMatch[3]}`;
570
+ }
571
+ // German Aktenzeichen: preserve structure
572
+ const azMatch = original.match(/^(\d{1,3})\s+([A-Z][a-z]*)\s+(\d{1,5})\/(\d{2,4})$/);
573
+ if (azMatch) {
574
+ const d = h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10));
575
+ return `${d.slice(0, azMatch[1].length)} ${azMatch[2]} ${d.slice(2, 2 + azMatch[3].length)}/${d.slice(5, 5 + azMatch[4].length)}`;
576
+ }
577
+ // Generic: preserve format character-by-character
578
+ const len = original.length || 10;
579
+ let result = "";
580
+ for (let i = 0; i < len; i++) {
581
+ const c = original[i];
582
+ if (!c || /[A-Z]/.test(c)) {
583
+ result += "ABCDEFGHJKLMNPRSTUVWXYZ"[parseInt(h[i * 2] || "0", 16) % 23];
584
+ }
585
+ else if (/[a-z]/.test(c)) {
586
+ result += "abcdefghjklmnprstuvwxyz"[parseInt(h[i * 2] || "0", 16) % 23];
587
+ }
588
+ else if (/[0-9]/.test(c)) {
589
+ result += String(parseInt(h[i] || "0", 16) % 10);
590
+ }
591
+ else {
592
+ result += c;
593
+ }
594
+ }
595
+ return result;
596
+ }
597
+ /**
598
+ * Fake cryptocurrency address.
599
+ * Preserves format: Ethereum (0x + 40 hex), Bitcoin P2PKH/P2SH (base58),
600
+ * Bitcoin Bech32 (bc1 + lowercase alphanum).
601
+ */
602
+ _fakeCryptoAddress(seed, original) {
603
+ const h = createHash("sha256").update(`crypto:${seed}`).digest("hex");
604
+ // Ethereum: 0x + 40 hex
605
+ if (/^0x[0-9a-fA-F]{40}$/.test(original)) {
606
+ return "0x" + h.slice(0, 40);
607
+ }
608
+ // Bitcoin Bech32: bc1 + lowercase alphanum
609
+ if (/^bc1[a-z0-9]{39,59}$/.test(original)) {
610
+ const len = original.length - 3; // minus "bc1"
611
+ const chars = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; // bech32 charset
612
+ let result = "bc1";
613
+ for (let i = 0; i < len; i++) {
614
+ result += chars[parseInt(h[(i * 2) % 64] + h[(i * 2 + 1) % 64], 16) % chars.length];
615
+ }
616
+ return result;
617
+ }
618
+ // Bitcoin P2PKH (starts with 1) or P2SH (starts with 3)
619
+ if (/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/.test(original)) {
620
+ const prefix = original[0];
621
+ const base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
622
+ const len = original.length - 1;
623
+ let result = prefix;
624
+ for (let i = 0; i < len; i++) {
625
+ result += base58[parseInt(h[(i * 2) % 64] + h[(i * 2 + 1) % 64], 16) % base58.length];
626
+ }
627
+ return result;
628
+ }
629
+ // Generic: preserve length with hex-like replacement
630
+ return h.slice(0, original.length || 42);
631
+ }
632
+ /**
633
+ * Fake AWS ARN.
634
+ * Preserves service and resource type, replaces account ID and resource name.
635
+ */
636
+ _fakeAwsArn(seed, original) {
637
+ const h = createHash("sha256").update(`arn:${seed}`).digest("hex");
638
+ // Full ARN: arn:partition:service:region:account-id:resource
639
+ const arnMatch = original.match(/^(arn:[a-z\-]+:[a-z0-9\-]+:[a-z0-9\-]*:)(\d{0,12}):(.+)$/);
640
+ if (arnMatch) {
641
+ const accountLen = arnMatch[2].length;
642
+ const fakeAccount = accountLen > 0
643
+ ? h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10)).slice(0, accountLen)
644
+ : ""; // S3-style ARN with no account
645
+ const resourceParts = arnMatch[3].split("/");
646
+ // Preserve resource type (first part), fake the rest
647
+ const fakeResource = resourceParts.length > 1
648
+ ? resourceParts[0] + "/shroud-" + h.slice(12, 20)
649
+ : "shroud-" + h.slice(12, 20);
650
+ return arnMatch[1] + fakeAccount + ":" + fakeResource;
651
+ }
652
+ // Bare account ID (12 digits)
653
+ if (/^\d{12}$/.test(original)) {
654
+ return h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10)).slice(0, 12);
655
+ }
656
+ return `arn:aws:iam::${h.replace(/[a-f]/g, c => String(parseInt(c, 16) % 10)).slice(0, 12)}:shroud-${h.slice(0, 8)}`;
657
+ }
231
658
  }
package/dist/types.d.ts CHANGED
@@ -29,6 +29,15 @@ export declare enum Category {
29
29
  ICS_IDENTIFIER = "ics_identifier",
30
30
  GPS_COORDINATE = "gps_coordinate",
31
31
  CERTIFICATE = "certificate",
32
+ DATE_OF_BIRTH = "date_of_birth",
33
+ MEDICAL_RECORD_NUMBER = "medical_record_number",
34
+ BANK_ACCOUNT_NUMBER = "bank_account_number",
35
+ TAX_ID = "tax_id",
36
+ PASSPORT_NUMBER = "passport_number",
37
+ DRIVERS_LICENSE = "drivers_license",
38
+ CASE_NUMBER = "case_number",
39
+ CRYPTOCURRENCY_ADDRESS = "cryptocurrency_address",
40
+ AWS_ARN = "aws_arn",
32
41
  CUSTOM = "custom"
33
42
  }
34
43
  /** A detected sensitive entity in text. */
package/dist/types.js CHANGED
@@ -31,5 +31,17 @@ export var Category;
31
31
  Category["ICS_IDENTIFIER"] = "ics_identifier";
32
32
  Category["GPS_COORDINATE"] = "gps_coordinate";
33
33
  Category["CERTIFICATE"] = "certificate";
34
+ // Healthcare / finance / identity
35
+ Category["DATE_OF_BIRTH"] = "date_of_birth";
36
+ Category["MEDICAL_RECORD_NUMBER"] = "medical_record_number";
37
+ Category["BANK_ACCOUNT_NUMBER"] = "bank_account_number";
38
+ Category["TAX_ID"] = "tax_id";
39
+ // Legal / identity documents
40
+ Category["PASSPORT_NUMBER"] = "passport_number";
41
+ Category["DRIVERS_LICENSE"] = "drivers_license";
42
+ Category["CASE_NUMBER"] = "case_number";
43
+ // Cloud / crypto
44
+ Category["CRYPTOCURRENCY_ADDRESS"] = "cryptocurrency_address";
45
+ Category["AWS_ARN"] = "aws_arn";
34
46
  Category["CUSTOM"] = "custom";
35
47
  })(Category || (Category = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shroud-privacy",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Privacy and infrastructure protection for AI agents — detects sensitive data (PII, network topology, credentials, OT/SCADA) and replaces with deterministic fakes before anything reaches the LLM.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",