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.
- package/dist/config-manager.js +4 -2
- package/dist/detectors/regex.d.ts +2 -0
- package/dist/detectors/regex.js +249 -2
- package/dist/generators/codes.d.ts +65 -0
- package/dist/generators/codes.js +435 -8
- package/dist/types.d.ts +9 -0
- package/dist/types.js +12 -0
- package/package.json +1 -1
package/dist/config-manager.js
CHANGED
|
@@ -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
|
-
|
|
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
|
};
|
package/dist/detectors/regex.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
}
|
package/dist/generators/codes.js
CHANGED
|
@@ -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
|
|
84
|
+
return this._fakeIcsId(seed, original);
|
|
76
85
|
}
|
|
77
86
|
else if (category === Category.CERTIFICATE) {
|
|
78
|
-
return
|
|
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
|
-
|
|
256
|
+
const h = createHash("sha256").update(`natid:${seed}`).digest("hex");
|
|
217
257
|
const len = original.length || 10;
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
//
|
|
227
|
-
const
|
|
228
|
-
|
|
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
|
+
"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",
|