ai-warden 1.0.2 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/package.json +1 -1
- package/src/pii/index.js +12 -60
- package/src/pii/patterns-presidio.json +44 -0
- package/src/pii/piiDetector-v2.js +215 -0
package/README.md
CHANGED
|
@@ -199,30 +199,56 @@ async function validateInput(text) {
|
|
|
199
199
|
const result = await validateInput(userInput);
|
|
200
200
|
```
|
|
201
201
|
|
|
202
|
-
### PII Detection &
|
|
202
|
+
### PII Detection & Handling (3 Modes)
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
const AIWarden = require('ai-warden');
|
|
206
|
-
const scanner = new AIWarden();
|
|
204
|
+
AI-Warden v1.0.3+ includes powerful PII detection with **3 handling modes**:
|
|
207
205
|
|
|
208
|
-
|
|
206
|
+
- **`ignore`** - Detect PII but don't modify text (just report findings)
|
|
207
|
+
- **`mask`** - Replace PII with labeled placeholders (`[EMAIL]`, `[SSN]`, etc.)
|
|
208
|
+
- **`remove`** - Remove PII completely from text
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
210
|
+
```javascript
|
|
211
|
+
const { PIIDetector, PII_MODES } = require('ai-warden/src/pii');
|
|
212
|
+
|
|
213
|
+
const text = 'Contact: john@example.com, SSN: 123-45-6789, Card: 5425-2334-3010-9903';
|
|
214
|
+
|
|
215
|
+
// Mode 1: IGNORE (detect only, don't modify)
|
|
216
|
+
const detector1 = new PIIDetector({ mode: PII_MODES.IGNORE });
|
|
217
|
+
const result1 = detector1.detect(text);
|
|
218
|
+
console.log(result1.hasPII); // true
|
|
219
|
+
console.log(result1.count); // 3
|
|
220
|
+
console.log(result1.findings); // Array of detected PII
|
|
221
|
+
console.log(result1.modified); // Original text (unchanged)
|
|
222
|
+
|
|
223
|
+
// Mode 2: MASK (replace with labels)
|
|
224
|
+
const detector2 = new PIIDetector({ mode: PII_MODES.MASK });
|
|
225
|
+
const result2 = detector2.detect(text);
|
|
226
|
+
console.log(result2.modified);
|
|
227
|
+
// "Contact: [EMAIL], SSN: [SSN], Card: [CREDIT_CARD]"
|
|
228
|
+
|
|
229
|
+
// Mode 3: REMOVE (delete PII completely)
|
|
230
|
+
const detector3 = new PIIDetector({ mode: PII_MODES.REMOVE });
|
|
231
|
+
const result3 = detector3.detect(text);
|
|
232
|
+
console.log(result3.modified);
|
|
233
|
+
// "Contact: , SSN: , Card: "
|
|
234
|
+
|
|
235
|
+
// Convenience methods (use default mode from config)
|
|
236
|
+
const detector = new PIIDetector();
|
|
237
|
+
console.log(detector.hasPII(text)); // true
|
|
238
|
+
console.log(detector.maskPII(text)); // Quick mask
|
|
239
|
+
console.log(detector.removePII(text)); // Quick remove
|
|
224
240
|
```
|
|
225
241
|
|
|
242
|
+
**Supported PII types:**
|
|
243
|
+
- Credit Cards (Visa, Mastercard, Amex, Discover) - with Luhn validation
|
|
244
|
+
- US SSN (Social Security Numbers)
|
|
245
|
+
- Emails (RFC 5322 compliant)
|
|
246
|
+
- Phone numbers (US + international formats)
|
|
247
|
+
- IPv4/IPv6 addresses
|
|
248
|
+
- Swedish Personnummer, Norwegian Fødselsnummer, Danish CPR, Finnish Henkilötunnus
|
|
249
|
+
- IBAN (European bank accounts)
|
|
250
|
+
- US Passports & Driver Licenses
|
|
251
|
+
|
|
226
252
|
---
|
|
227
253
|
|
|
228
254
|
## 🎮 CLI Usage
|
package/package.json
CHANGED
package/src/pii/index.js
CHANGED
|
@@ -1,65 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PII Detection
|
|
3
|
-
*
|
|
4
|
-
* Detects and masks personally identifiable information (PII).
|
|
2
|
+
* PII Detection Module
|
|
3
|
+
* Exports both v1 (legacy) and v2 (Presidio-based) detectors
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
const
|
|
6
|
+
const PIIDetectorV1 = require('./piiDetector');
|
|
7
|
+
const PIIDetectorV2 = require('./piiDetector-v2');
|
|
8
|
+
const { PII_MODES } = require('./piiDetector-v2');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
function detect(content, options = {}) {
|
|
13
|
-
const settings = {
|
|
14
|
-
email: options.email !== false,
|
|
15
|
-
phone: options.phone !== false,
|
|
16
|
-
ssn: options.ssn !== false,
|
|
17
|
-
creditCard: options.creditCard !== false,
|
|
18
|
-
ipAddress: options.ipAddress !== false,
|
|
19
|
-
...options
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
return piiDetector.detect(content, settings);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Mask PII in content
|
|
27
|
-
*/
|
|
28
|
-
function mask(content, findings, options = {}) {
|
|
29
|
-
const maskChar = options.maskChar || '*';
|
|
30
|
-
const preserveLength = options.preserveLength !== false;
|
|
31
|
-
|
|
32
|
-
let masked = content;
|
|
33
|
-
|
|
34
|
-
// Sort findings by position (reverse order to maintain indices)
|
|
35
|
-
const sorted = [...findings].sort((a, b) => b.start - a.start);
|
|
36
|
-
|
|
37
|
-
for (const finding of sorted) {
|
|
38
|
-
const before = masked.substring(0, finding.start);
|
|
39
|
-
const after = masked.substring(finding.end);
|
|
40
|
-
|
|
41
|
-
let replacement;
|
|
42
|
-
if (preserveLength) {
|
|
43
|
-
replacement = maskChar.repeat(finding.end - finding.start);
|
|
44
|
-
} else {
|
|
45
|
-
replacement = `[${finding.type.toUpperCase()}_REDACTED]`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
masked = before + replacement + after;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return masked;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Redact PII in content (replace with labels)
|
|
56
|
-
*/
|
|
57
|
-
function redact(content, findings) {
|
|
58
|
-
return mask(content, findings, { preserveLength: false });
|
|
59
|
-
}
|
|
10
|
+
// Export v2 as default (Presidio-based, production-ready)
|
|
11
|
+
module.exports = PIIDetectorV2;
|
|
60
12
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
13
|
+
// Also export named exports for flexibility
|
|
14
|
+
module.exports.PIIDetector = PIIDetectorV2;
|
|
15
|
+
module.exports.PIIDetectorV1 = PIIDetectorV1; // Legacy, deprecated
|
|
16
|
+
module.exports.PIIDetectorV2 = PIIDetectorV2;
|
|
17
|
+
module.exports.PII_MODES = PII_MODES;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0",
|
|
3
|
+
"lastUpdated": "2026-02-22",
|
|
4
|
+
"description": "Presidio-based PII patterns (simplified, working)",
|
|
5
|
+
"patterns": {
|
|
6
|
+
"creditCard": {
|
|
7
|
+
"name": "Credit Card Number",
|
|
8
|
+
"description": "Visa, Mastercard, Amex, Discover",
|
|
9
|
+
"regex": "\\b(?:4\\d{3}|5[1-5]\\d{2}|3[47]\\d{2}|6(?:011|5\\d{2}))[-\\s]?\\d{4}[-\\s]?\\d{4}[-\\s]?\\d{4}\\b",
|
|
10
|
+
"confidenceScore": 0.3,
|
|
11
|
+
"requiresValidation": true,
|
|
12
|
+
"validationMethod": "luhn"
|
|
13
|
+
},
|
|
14
|
+
"usSSN": {
|
|
15
|
+
"name": "US Social Security Number",
|
|
16
|
+
"description": "XXX-XX-XXXX format",
|
|
17
|
+
"regex": "\\b\\d{3}-\\d{2}-\\d{4}\\b",
|
|
18
|
+
"confidenceScore": 0.5,
|
|
19
|
+
"requiresValidation": false,
|
|
20
|
+
"validationMethod": "custom"
|
|
21
|
+
},
|
|
22
|
+
"email": {
|
|
23
|
+
"name": "Email Address",
|
|
24
|
+
"description": "Standard email format",
|
|
25
|
+
"regex": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b",
|
|
26
|
+
"confidenceScore": 0.6,
|
|
27
|
+
"requiresValidation": false
|
|
28
|
+
},
|
|
29
|
+
"phone": {
|
|
30
|
+
"name": "Phone Number",
|
|
31
|
+
"description": "US phone format",
|
|
32
|
+
"regex": "\\b(?:\\+?1[-.]?)?\\(?\\d{3}\\)?[-.]?\\d{3}[-.]?\\d{4}\\b",
|
|
33
|
+
"confidenceScore": 0.4,
|
|
34
|
+
"requiresValidation": false
|
|
35
|
+
},
|
|
36
|
+
"ipv4": {
|
|
37
|
+
"name": "IPv4 Address",
|
|
38
|
+
"description": "Standard IPv4",
|
|
39
|
+
"regex": "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b",
|
|
40
|
+
"confidenceScore": 0.8,
|
|
41
|
+
"requiresValidation": false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII Detection Service v2 (Presidio-based)
|
|
3
|
+
* Production-ready pattern detection with validation and configurable handling modes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const piiPatterns = require('./patterns-presidio.json');
|
|
7
|
+
const validators = require('./validators');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PII Handling Modes
|
|
11
|
+
* - 'remove': Remove detected PII completely (e.g., "My SSN is 123-45-6789" → "My SSN is ")
|
|
12
|
+
* - 'mask': Mask detected PII (e.g., "My SSN is 123-45-6789" → "My SSN is [SSN_REDACTED]")
|
|
13
|
+
* - 'ignore': Do nothing, just detect and report (no modification)
|
|
14
|
+
*/
|
|
15
|
+
const PII_MODES = {
|
|
16
|
+
REMOVE: 'remove',
|
|
17
|
+
MASK: 'mask',
|
|
18
|
+
IGNORE: 'ignore'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class PIIDetector {
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.patterns = piiPatterns.patterns;
|
|
24
|
+
this.config = {
|
|
25
|
+
mode: config.mode || PII_MODES.IGNORE, // Default: just detect, don't modify
|
|
26
|
+
tier: config.tier || 'free',
|
|
27
|
+
validateChecksums: config.validateChecksums !== false,
|
|
28
|
+
contextAnalysis: config.contextAnalysis !== false,
|
|
29
|
+
confidenceThreshold: config.confidenceThreshold || 0.5,
|
|
30
|
+
...config
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect and optionally redact PII in text
|
|
36
|
+
* @param {string} text - Input text to scan
|
|
37
|
+
* @param {object} options - Detection options
|
|
38
|
+
* @returns {object} Detection results with optionally modified text
|
|
39
|
+
*/
|
|
40
|
+
detect(text, options = {}) {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
types = ['all'],
|
|
45
|
+
mode = this.config.mode, // Override default mode
|
|
46
|
+
tier = this.config.tier
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const findings = [];
|
|
50
|
+
const replacements = []; // Track text replacements for remove/mask modes
|
|
51
|
+
let modifiedText = text;
|
|
52
|
+
|
|
53
|
+
// Scan all pattern types
|
|
54
|
+
for (const [patternKey, pattern] of Object.entries(this.patterns)) {
|
|
55
|
+
// Skip if not in requested types
|
|
56
|
+
if (types[0] !== 'all' && !types.includes(patternKey)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Skip tier-restricted patterns
|
|
61
|
+
if (pattern.tier && tier === 'free' && pattern.tier !== 'free') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const regex = new RegExp(pattern.regex, 'g');
|
|
66
|
+
let match;
|
|
67
|
+
|
|
68
|
+
while ((match = regex.exec(text)) !== null) {
|
|
69
|
+
const candidate = match[0];
|
|
70
|
+
const startPos = match.index;
|
|
71
|
+
const endPos = startPos + candidate.length;
|
|
72
|
+
|
|
73
|
+
// Validate if required
|
|
74
|
+
let isValid = true;
|
|
75
|
+
if (pattern.requiresValidation) {
|
|
76
|
+
isValid = this._validate(candidate, pattern);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isValid) {
|
|
80
|
+
const finding = {
|
|
81
|
+
type: patternKey,
|
|
82
|
+
name: pattern.name,
|
|
83
|
+
value: candidate,
|
|
84
|
+
start: startPos,
|
|
85
|
+
end: endPos,
|
|
86
|
+
confidence: pattern.confidenceScore || 0.5,
|
|
87
|
+
validated: pattern.requiresValidation
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
findings.push(finding);
|
|
91
|
+
|
|
92
|
+
// Track replacement if mode is remove/mask
|
|
93
|
+
if (mode === PII_MODES.REMOVE || mode === PII_MODES.MASK) {
|
|
94
|
+
replacements.push({
|
|
95
|
+
start: startPos,
|
|
96
|
+
end: endPos,
|
|
97
|
+
original: candidate,
|
|
98
|
+
replacement: mode === PII_MODES.MASK ? this._getMaskText(patternKey) : ''
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply replacements (in reverse order to preserve positions)
|
|
106
|
+
if (replacements.length > 0) {
|
|
107
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
108
|
+
|
|
109
|
+
for (const rep of replacements) {
|
|
110
|
+
modifiedText =
|
|
111
|
+
modifiedText.substring(0, rep.start) +
|
|
112
|
+
rep.replacement +
|
|
113
|
+
modifiedText.substring(rep.end);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
hasPII: findings.length > 0,
|
|
119
|
+
count: findings.length,
|
|
120
|
+
findings,
|
|
121
|
+
mode,
|
|
122
|
+
original: text,
|
|
123
|
+
modified: modifiedText,
|
|
124
|
+
changed: text !== modifiedText,
|
|
125
|
+
processingTime: Date.now() - startTime
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate PII candidate using appropriate validation method
|
|
131
|
+
*/
|
|
132
|
+
_validate(value, pattern) {
|
|
133
|
+
if (!pattern.validationMethod) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
switch (pattern.validationMethod) {
|
|
138
|
+
case 'luhn':
|
|
139
|
+
return validators.luhnValidate(value);
|
|
140
|
+
|
|
141
|
+
case 'iban':
|
|
142
|
+
return validators.ibanValidate(value);
|
|
143
|
+
|
|
144
|
+
case 'custom':
|
|
145
|
+
// Custom validators per type
|
|
146
|
+
if (pattern.name.includes('SSN')) {
|
|
147
|
+
return validators.usSsnValidate(value);
|
|
148
|
+
}
|
|
149
|
+
if (pattern.name.includes('personnummer')) {
|
|
150
|
+
return validators.swedishSSNValidate(value);
|
|
151
|
+
}
|
|
152
|
+
if (pattern.name.includes('Fødselsnummer')) {
|
|
153
|
+
return validators.norwegianNinValidate(value);
|
|
154
|
+
}
|
|
155
|
+
if (pattern.name.includes('Henkilötunnus')) {
|
|
156
|
+
return validators.finnishHetuValidate(value);
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
|
|
160
|
+
default:
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get mask text for a PII type
|
|
167
|
+
*/
|
|
168
|
+
_getMaskText(patternKey) {
|
|
169
|
+
const maskMap = {
|
|
170
|
+
creditCard: '[CREDIT_CARD]',
|
|
171
|
+
usSSN: '[SSN]',
|
|
172
|
+
swedishPersonnummer: '[PERSONNUMMER]',
|
|
173
|
+
norwegianNIN: '[FØDSELSNUMMER]',
|
|
174
|
+
danishCPR: '[CPR]',
|
|
175
|
+
finnishPersonID: '[HENKILÖTUNNUS]',
|
|
176
|
+
email: '[EMAIL]',
|
|
177
|
+
phone: '[PHONE]',
|
|
178
|
+
ipv4: '[IP_ADDRESS]',
|
|
179
|
+
ipv6: '[IP_ADDRESS]',
|
|
180
|
+
iban: '[IBAN]',
|
|
181
|
+
usPassport: '[PASSPORT]',
|
|
182
|
+
usDriverLicense: '[DRIVER_LICENSE]',
|
|
183
|
+
date: '[DATE]'
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return maskMap[patternKey] || '[PII_REDACTED]';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Quick check: does text contain any PII?
|
|
191
|
+
*/
|
|
192
|
+
hasPII(text) {
|
|
193
|
+
const result = this.detect(text, { mode: PII_MODES.IGNORE });
|
|
194
|
+
return result.hasPII;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Remove all PII from text
|
|
199
|
+
*/
|
|
200
|
+
removePII(text) {
|
|
201
|
+
const result = this.detect(text, { mode: PII_MODES.REMOVE });
|
|
202
|
+
return result.modified;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mask all PII in text
|
|
207
|
+
*/
|
|
208
|
+
maskPII(text) {
|
|
209
|
+
const result = this.detect(text, { mode: PII_MODES.MASK });
|
|
210
|
+
return result.modified;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = PIIDetector;
|
|
215
|
+
module.exports.PII_MODES = PII_MODES;
|