Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
296 lines
8.6 KiB
TypeScript
296 lines
8.6 KiB
TypeScript
// Source: https://github.com/openai/openai-guardrails-js/blob/b9b99b4fb454f02a362c2836aec6285176ec40a8/src/checks/pii.ts
|
|
/**
|
|
* PII detection guardrail for sensitive text content.
|
|
*
|
|
* This module implements a guardrail for detecting Personally Identifiable
|
|
* Information (PII) in text using regex patterns. It defines the config
|
|
* schema for entity selection, output/result structures, and the async guardrail
|
|
* check_fn for runtime enforcement.
|
|
*/
|
|
|
|
import { parseRegex } from '../../helpers/common';
|
|
import type { CreateCheckFn, CustomRegex } from '../types';
|
|
|
|
/**
|
|
* Supported PII entity types for detection.
|
|
*
|
|
* Includes global and region-specific types (US, UK, Spain, Italy, etc.).
|
|
* These map to regex patterns for detection.
|
|
*/
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
export enum PIIEntity {
|
|
// Global
|
|
CREDIT_CARD = 'CREDIT_CARD',
|
|
CRYPTO = 'CRYPTO',
|
|
DATE_TIME = 'DATE_TIME',
|
|
EMAIL_ADDRESS = 'EMAIL_ADDRESS',
|
|
IBAN_CODE = 'IBAN_CODE',
|
|
IP_ADDRESS = 'IP_ADDRESS',
|
|
LOCATION = 'LOCATION',
|
|
PHONE_NUMBER = 'PHONE_NUMBER',
|
|
MEDICAL_LICENSE = 'MEDICAL_LICENSE',
|
|
|
|
// USA
|
|
US_BANK_NUMBER = 'US_BANK_NUMBER',
|
|
US_DRIVER_LICENSE = 'US_DRIVER_LICENSE',
|
|
US_ITIN = 'US_ITIN',
|
|
US_PASSPORT = 'US_PASSPORT',
|
|
US_SSN = 'US_SSN',
|
|
|
|
// UK
|
|
UK_NHS = 'UK_NHS',
|
|
UK_NINO = 'UK_NINO',
|
|
|
|
// Spain
|
|
ES_NIF = 'ES_NIF',
|
|
ES_NIE = 'ES_NIE',
|
|
|
|
// Italy
|
|
IT_FISCAL_CODE = 'IT_FISCAL_CODE',
|
|
IT_DRIVER_LICENSE = 'IT_DRIVER_LICENSE',
|
|
IT_VAT_CODE = 'IT_VAT_CODE',
|
|
IT_PASSPORT = 'IT_PASSPORT',
|
|
IT_IDENTITY_CARD = 'IT_IDENTITY_CARD',
|
|
|
|
// Poland
|
|
PL_PESEL = 'PL_PESEL',
|
|
|
|
// Singapore
|
|
SG_NRIC_FIN = 'SG_NRIC_FIN',
|
|
SG_UEN = 'SG_UEN',
|
|
|
|
// Australia
|
|
AU_ABN = 'AU_ABN',
|
|
AU_ACN = 'AU_ACN',
|
|
AU_TFN = 'AU_TFN',
|
|
AU_MEDICARE = 'AU_MEDICARE',
|
|
|
|
// India
|
|
IN_PAN = 'IN_PAN',
|
|
IN_AADHAAR = 'IN_AADHAAR',
|
|
IN_VEHICLE_REGISTRATION = 'IN_VEHICLE_REGISTRATION',
|
|
IN_VOTER = 'IN_VOTER',
|
|
IN_PASSPORT = 'IN_PASSPORT',
|
|
|
|
// Finland
|
|
FI_PERSONAL_IDENTITY_CODE = 'FI_PERSONAL_IDENTITY_CODE',
|
|
}
|
|
|
|
const allEntities = Object.values(PIIEntity);
|
|
|
|
export type PIIConfig = {
|
|
entities?: PIIEntity[];
|
|
customRegex?: CustomRegex[];
|
|
};
|
|
|
|
export type CustomRegexConfig = {
|
|
customRegex: CustomRegex[];
|
|
};
|
|
|
|
/**
|
|
* Internal result structure for PII detection.
|
|
*/
|
|
interface PiiDetectionResult {
|
|
mapping: Record<string, string[]>;
|
|
analyzerResults: PiiAnalyzerResult[];
|
|
}
|
|
|
|
/**
|
|
* PII analyzer result structure.
|
|
*/
|
|
interface PiiAnalyzerResult {
|
|
entityType: string;
|
|
text: string;
|
|
}
|
|
|
|
export const PII_NAME_MAP: Record<PIIEntity, string> = {
|
|
[PIIEntity.CREDIT_CARD]: 'Credit Card',
|
|
[PIIEntity.CRYPTO]: 'Crypto',
|
|
[PIIEntity.DATE_TIME]: 'Date Time',
|
|
[PIIEntity.EMAIL_ADDRESS]: 'Email Address',
|
|
[PIIEntity.IBAN_CODE]: 'IBAN Code',
|
|
[PIIEntity.IP_ADDRESS]: 'IP Address',
|
|
[PIIEntity.LOCATION]: 'Location',
|
|
[PIIEntity.PHONE_NUMBER]: 'Phone Number',
|
|
[PIIEntity.MEDICAL_LICENSE]: 'Medical License',
|
|
[PIIEntity.US_BANK_NUMBER]: 'US Bank Number',
|
|
[PIIEntity.US_DRIVER_LICENSE]: 'US Driver License',
|
|
[PIIEntity.US_ITIN]: 'US ITIN',
|
|
[PIIEntity.US_PASSPORT]: 'US Passport',
|
|
[PIIEntity.US_SSN]: 'US SSN',
|
|
[PIIEntity.UK_NHS]: 'UK NHS',
|
|
[PIIEntity.UK_NINO]: 'UK NINO',
|
|
[PIIEntity.ES_NIF]: 'ES NIF',
|
|
[PIIEntity.ES_NIE]: 'ES NIE',
|
|
[PIIEntity.IT_FISCAL_CODE]: 'IT Fiscal Code',
|
|
[PIIEntity.IT_DRIVER_LICENSE]: 'IT Driver License',
|
|
[PIIEntity.IT_VAT_CODE]: 'IT VAT Code',
|
|
[PIIEntity.IT_PASSPORT]: 'IT Passport',
|
|
[PIIEntity.IT_IDENTITY_CARD]: 'IT Identity Card',
|
|
[PIIEntity.PL_PESEL]: 'PL PESEL',
|
|
[PIIEntity.SG_NRIC_FIN]: 'SG NRIC FIN',
|
|
[PIIEntity.SG_UEN]: 'SG UEN',
|
|
[PIIEntity.AU_ABN]: 'AU ABN',
|
|
[PIIEntity.AU_ACN]: 'AU ACN',
|
|
[PIIEntity.AU_TFN]: 'AU TFN',
|
|
[PIIEntity.AU_MEDICARE]: 'AU Medicare',
|
|
[PIIEntity.IN_PAN]: 'IN PAN',
|
|
[PIIEntity.IN_AADHAAR]: 'IN AADHAAR',
|
|
[PIIEntity.IN_VEHICLE_REGISTRATION]: 'IN Vehicle Registration',
|
|
[PIIEntity.IN_VOTER]: 'IN Voter',
|
|
[PIIEntity.IN_PASSPORT]: 'IN Passport',
|
|
[PIIEntity.FI_PERSONAL_IDENTITY_CODE]: 'FI Personal Identity Code',
|
|
};
|
|
|
|
/**
|
|
* Default regex patterns for PII entity types.
|
|
*/
|
|
const DEFAULT_PII_PATTERNS: Record<PIIEntity, RegExp> = {
|
|
[PIIEntity.CREDIT_CARD]: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
|
|
[PIIEntity.CRYPTO]: /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/g,
|
|
[PIIEntity.DATE_TIME]: /\b(0[1-9]|1[0-2])[\/\-](0[1-9]|[12]\d|3[01])[\/\-](19|20)\d{2}\b/g,
|
|
[PIIEntity.EMAIL_ADDRESS]: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
[PIIEntity.IBAN_CODE]: /\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}([A-Z0-9]?){0,16}\b/g,
|
|
[PIIEntity.IP_ADDRESS]: /\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b/g,
|
|
[PIIEntity.LOCATION]:
|
|
/\b[A-Za-z\s]+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Drive|Dr|Lane|Ln|Place|Pl|Court|Ct|Way|Highway|Hwy)\b/g,
|
|
[PIIEntity.PHONE_NUMBER]: /\b[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}\b/g,
|
|
[PIIEntity.MEDICAL_LICENSE]: /\b[A-Z]{2}\d{6}\b/g,
|
|
|
|
// USA
|
|
[PIIEntity.US_BANK_NUMBER]: /\b\d{8,17}\b/g,
|
|
[PIIEntity.US_DRIVER_LICENSE]: /\b[A-Z]\d{7}\b/g,
|
|
[PIIEntity.US_ITIN]: /\b9\d{2}-\d{2}-\d{4}\b/g,
|
|
[PIIEntity.US_PASSPORT]: /\b[A-Z]\d{8}\b/g,
|
|
[PIIEntity.US_SSN]: /\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b/g,
|
|
|
|
// UK
|
|
[PIIEntity.UK_NHS]: /\b\d{3} \d{3} \d{4}\b/g,
|
|
[PIIEntity.UK_NINO]: /\b[A-Z]{2}\d{6}[A-Z]\b/g,
|
|
|
|
// Spain
|
|
[PIIEntity.ES_NIF]: /\b[A-Z]\d{8}\b/g,
|
|
[PIIEntity.ES_NIE]: /\b[A-Z]\d{8}\b/g,
|
|
|
|
// Italy
|
|
[PIIEntity.IT_FISCAL_CODE]: /\b[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]\b/g,
|
|
[PIIEntity.IT_DRIVER_LICENSE]: /\b[A-Z]{2}\d{7}\b/g,
|
|
[PIIEntity.IT_VAT_CODE]: /\bIT\d{11}\b/g,
|
|
[PIIEntity.IT_PASSPORT]: /\b[A-Z]{2}\d{7}\b/g,
|
|
[PIIEntity.IT_IDENTITY_CARD]: /\b[A-Z]{2}\d{7}\b/g,
|
|
|
|
// Poland
|
|
[PIIEntity.PL_PESEL]: /\b\d{11}\b/g,
|
|
|
|
// Singapore
|
|
[PIIEntity.SG_NRIC_FIN]: /\b[A-Z]\d{7}[A-Z]\b/g,
|
|
[PIIEntity.SG_UEN]: /\b\d{8}[A-Z]\b|\b\d{9}[A-Z]\b/g,
|
|
|
|
// Australia
|
|
[PIIEntity.AU_ABN]: /\b\d{2} \d{3} \d{3} \d{3}\b/g,
|
|
[PIIEntity.AU_ACN]: /\b\d{3} \d{3} \d{3}\b/g,
|
|
[PIIEntity.AU_TFN]: /\b\d{9}\b/g,
|
|
[PIIEntity.AU_MEDICARE]: /\b\d{4} \d{5} \d{1}\b/g,
|
|
|
|
// India
|
|
[PIIEntity.IN_PAN]: /\b[A-Z]{5}\d{4}[A-Z]\b/g,
|
|
[PIIEntity.IN_AADHAAR]: /\b\d{4} \d{4} \d{4}\b/g,
|
|
[PIIEntity.IN_VEHICLE_REGISTRATION]: /\b[A-Z]{2}\d{2}[A-Z]{2}\d{4}\b/g,
|
|
[PIIEntity.IN_VOTER]: /\b[A-Z]{3}\d{7}\b/g,
|
|
[PIIEntity.IN_PASSPORT]: /\b[A-Z]\d{7}\b/g,
|
|
|
|
// Finland
|
|
[PIIEntity.FI_PERSONAL_IDENTITY_CODE]: /\b\d{6}[+-A]\d{3}[A-Z0-9]\b/g,
|
|
};
|
|
|
|
/**
|
|
* Run regex analysis and collect findings by entity type.
|
|
*
|
|
* @param text The text to analyze for PII
|
|
* @param config PII detection configuration
|
|
* @returns Object containing mapping of entities to detected snippets
|
|
* @throws Error if text is empty or null
|
|
*/
|
|
function detectPii(text: string, config: PIIConfig): PiiDetectionResult {
|
|
if (!text) {
|
|
return {
|
|
mapping: {},
|
|
analyzerResults: [],
|
|
};
|
|
}
|
|
|
|
const grouped: Record<string, string[]> = {};
|
|
const analyzerResults: PiiAnalyzerResult[] = [];
|
|
|
|
const matchAgainstPattern = (name: string, pattern: RegExp) => {
|
|
// make sure to add the global flag to the regex, otherwise while() will never end
|
|
const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g';
|
|
const regex = new RegExp(pattern.source, flags);
|
|
let match;
|
|
while ((match = regex.exec(text)) !== null) {
|
|
const entityType = name;
|
|
const start = match.index;
|
|
const end = match.index + match[0].length;
|
|
|
|
if (!grouped[entityType]) {
|
|
grouped[entityType] = [];
|
|
}
|
|
grouped[entityType].push(text.substring(start, end));
|
|
|
|
analyzerResults.push({
|
|
entityType,
|
|
text: text.substring(start, end),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Check each configured entity type
|
|
const entities = config.entities ?? allEntities;
|
|
for (const entity of entities) {
|
|
const pattern = DEFAULT_PII_PATTERNS[entity];
|
|
if (pattern) {
|
|
matchAgainstPattern(entity, pattern);
|
|
}
|
|
}
|
|
if (config.customRegex?.length) {
|
|
for (const regex of config.customRegex) {
|
|
matchAgainstPattern(regex.name, parseRegex(regex.value));
|
|
}
|
|
}
|
|
|
|
return {
|
|
mapping: grouped,
|
|
analyzerResults,
|
|
};
|
|
}
|
|
|
|
export const createPiiCheckFn: CreateCheckFn<PIIConfig> = (config) => {
|
|
return (input: string) => {
|
|
const detection = detectPii(input, config);
|
|
const piiFound = detection.mapping && Object.keys(detection.mapping).length > 0;
|
|
return {
|
|
guardrailName: 'personalData',
|
|
tripwireTriggered: piiFound,
|
|
info: {
|
|
maskEntities: detection.mapping,
|
|
analyzerResults: detection.analyzerResults,
|
|
},
|
|
};
|
|
};
|
|
};
|
|
|
|
export const createCustomRegexCheckFn: CreateCheckFn<CustomRegexConfig> = (config) => {
|
|
return (input: string) => {
|
|
const detection = detectPii(input, { customRegex: config.customRegex, entities: [] });
|
|
const customRegexFound = detection.mapping && Object.keys(detection.mapping).length > 0;
|
|
return {
|
|
guardrailName: 'customRegex',
|
|
tripwireTriggered: customRegexFound,
|
|
info: {
|
|
maskEntities: detection.mapping,
|
|
analyzerResults: detection.analyzerResults,
|
|
},
|
|
};
|
|
};
|
|
};
|