diff --git a/src/lib/privacy-innovations.ts b/src/lib/privacy-innovations.ts index 091b8d5..f51e011 100644 --- a/src/lib/privacy-innovations.ts +++ b/src/lib/privacy-innovations.ts @@ -1,4 +1,4 @@ -import type { PrivacyLevel } from "./privacy-layer"; +import type { PrivacyLevel } from "./privacy-layer"; // ============================================================ // 1. PRIVACY PERSONAS @@ -148,7 +148,7 @@ export const PERSONAS: Record = { */ export function detectPersona(request: string, data?: string): PersonaDetectionResult { const text = `${request} ${data || ""}`.toLowerCase(); - + let bestPersona: PersonaId = "auto"; let bestScore = 0; const triggeredKeywords: string[] = []; @@ -157,14 +157,14 @@ export function detectPersona(request: string, data?: string): PersonaDetectionR if (id === "auto") continue; let score = 0; const hits: string[] = []; - + for (const keyword of persona.autoDetectKeywords) { if (text.includes(keyword.toLowerCase())) { score += 1; hits.push(keyword); } } - + if (score > bestScore) { bestScore = score; bestPersona = id as PersonaId; @@ -175,7 +175,7 @@ export function detectPersona(request: string, data?: string): PersonaDetectionR // Confidence calculation const confidence = Math.min(100, bestScore * 15); - + return { detectedPersona: bestPersona, confidence, @@ -209,7 +209,7 @@ export function applyPersonaRules( const persona = PERSONAS[personaId]; const appliedRules: HardRule[] = []; let explanation = `Base level (${baseLevel}) selected by sensitivity analysis.`; - + // Apply hard rules in order, collecting all rules as we go for (const rule of persona.hardRules) { appliedRules.push(rule); @@ -259,7 +259,7 @@ export function applyPersonaRules( return { finalLevel: persona.defaultLevel, appliedRules, explanation }; } } - + return { finalLevel: baseLevel, appliedRules, explanation }; } @@ -329,7 +329,7 @@ function extractEntities(text: string): EntityMapping[] { const mappings: EntityMapping[] = []; let nameIdx = 0; let orgIdx = 0; - + // Names const names = extractNames(text); for (const name of names) { @@ -337,28 +337,28 @@ function extractEntities(text: string): EntityMapping[] { mappings.push({ original: name, synthetic: SYNTHETIC_NAMES[nameIdx++], type: "name" }); } } - + // SSN patterns — only match formatted SSNs (NNN-NN-NNNN) to avoid false positives on zip codes, IDs, etc. const ssnPattern = /\b\d{3}-\d{2}-\d{4}\b/g; let ssnMatch; while ((ssnMatch = ssnPattern.exec(text)) !== null) { mappings.push({ original: ssnMatch[0], synthetic: generateSyntheticId("ssn"), type: "ssn" }); } - + // Email patterns const emailPattern = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g; let emailMatch; while ((emailMatch = emailPattern.exec(text)) !== null) { mappings.push({ original: emailMatch[0], synthetic: generateSyntheticId("email"), type: "email" }); } - + // Phone patterns const phonePattern = /\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g; let phoneMatch; while ((phoneMatch = phonePattern.exec(text)) !== null) { mappings.push({ original: phoneMatch[0], synthetic: generateSyntheticId("phone"), type: "phone" }); } - + // Organizations (capitalized multi-word phrases not caught as names) const orgPattern = /(?:Corp|Inc|LLC|Ltd|Company|Ventures|Partners|Group|Systems|Labs|Industries|Technologies)/gi; let orgMatch; @@ -370,7 +370,7 @@ function extractEntities(text: string): EntityMapping[] { mappings.push({ original: orgName, synthetic: SYNTHETIC_ORGS[orgIdx++ % SYNTHETIC_ORGS.length], type: "organization" }); } } - + // Dollar amounts (preserve range but mask exact) const amountPattern = /\$[\d,]+(?:\.\d{2})?\s*(?:M|K|Billion|Million)?/gi; let amountMatch; @@ -384,21 +384,21 @@ function extractEntities(text: string): EntityMapping[] { else synthetic = "$XXX.XX"; mappings.push({ original: amountMatch[0], synthetic, type: "amount" }); } - + // Patient/record IDs const idPattern = /(?:patient\s*id|mrn|record\s*#|id[:\s]+)\s*[:#]?\s*(\d+)/gi; let idMatch; while ((idMatch = idPattern.exec(text)) !== null) { mappings.push({ original: idMatch[0], synthetic: `ID: ${generateSyntheticId("mrn")}`, type: "id" }); } - + // API keys / credentials const credPattern = /(?:AKIA[0-9A-Z]{16}|ghp_[a-zA-Z0-9]{36}|sk-[a-zA-Z0-9]{32,}|api[_-]?key[:\s]+[a-zA-Z0-9_-]{8,})/gi; let credMatch; while ((credMatch = credPattern.exec(text)) !== null) { mappings.push({ original: credMatch[0], synthetic: "[REDACTED-CREDENTIAL]", type: "credential" }); } - + return mappings; } @@ -443,24 +443,24 @@ function extractEntities(text: string): EntityMapping[] { */ export function applySyntheticSubstitution(text: string): SyntheticResult { const mappings = extractEntities(text); - + // Sort by length (descending) to avoid partial replacements const sortedMappings = [...mappings].sort((a, b) => b.original.length - a.original.length); - + let transformedText = text; for (const mapping of sortedMappings) { // Use a global replace with escape for regex special chars const escaped = mapping.original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); transformedText = transformedText.replace(new RegExp(escaped, "g"), mapping.synthetic); } - + // Collect stats preserved const statsPreserved: string[] = []; if (mappings.some(m => m.type === "name")) statsPreserved.push("Entity relationships preserved"); if (mappings.some(m => m.type === "amount")) statsPreserved.push("Value ranges preserved"); if (mappings.some(m => m.type === "ssn" || m.type === "id")) statsPreserved.push("Record structure preserved"); if (mappings.length > 0) statsPreserved.push("Semantic patterns maintained"); - + return { transformedText, mappings, @@ -572,32 +572,32 @@ export function embedWatermark( const timestamp = Date.now(); const queryHash = hashString(query).slice(0, 16); const userFingerprint = hashString(navigator?.userAgent || "unknown").slice(0, 8); - + const metadata: WatermarkMetadata = { timestamp, privacyLevel, queryHash, userFingerprint, }; - + // Encode metadata as binary string // Format: [timestamp(48 bits)][level(3 bits)][queryHash(16 bits)][fingerprint(8 bits)] const tsBits = timestamp.toString(2).padStart(48, "0"); const levelBits = ["LOW", "STANDARD", "MEDIUM", "HIGH", "MAXIMUM"].indexOf(privacyLevel).toString(2).padStart(3, "0"); const hashBits = queryHash; const fpBits = userFingerprint; - + const fullBits = tsBits + levelBits + hashBits + fpBits; const watermark = encodeBitsToWatermark(fullBits); - + // Insert watermark after first sentence (or at end if no sentence break) const sentenceEnd = text.search(/[.!?]\s/); const insertPos = sentenceEnd > 0 ? sentenceEnd + 1 : Math.floor(text.length / 2); - + const watermarkedText = text.slice(0, insertPos) + watermark + text.slice(insertPos); - + const humanReadable = `Privacy: ${privacyLevel} | Query: ${queryHash.slice(0, 6)}... | Time: ${new Date(timestamp).toISOString()} | User: ${userFingerprint.slice(0, 4)}...`; - + return { watermarkedText, metadata, @@ -627,27 +627,27 @@ export function embedWatermark( */ export function detectWatermark(text: string): WatermarkDetection { const bits = decodeWatermarkFromText(text); - + if (bits.length < 48) { return { detected: false, confidence: 0 }; } - + // Check if the first 48 bits decode to a reasonable timestamp const tsBits = bits.slice(0, 48); const timestamp = parseInt(tsBits, 2); const now = Date.now(); const isValidTimestamp = timestamp > 1700000000000 && timestamp <= now + 86400000; - + if (!isValidTimestamp) { return { detected: false, confidence: 0 }; } - + const levelIdx = parseInt(bits.slice(48, 51), 2); const allLevels: PrivacyLevel[] = ["LOW", "STANDARD", "MEDIUM", "HIGH", "MAXIMUM"]; const level: PrivacyLevel = allLevels[levelIdx] ?? "LOW"; const queryHash = bits.slice(51, 67); const userFingerprint = bits.slice(67, 75); - + return { detected: true, metadata: { @@ -740,17 +740,17 @@ export async function processEnhanced(req: EnhancedProcessRequest): Promise 10000) { throw new Error("Request exceeds maximum length of 10,000 characters"); } - + if (req.data && req.data.length > 100000) { throw new Error("Data exceeds maximum length of 100,000 characters"); } - + const { processWithPrivacy } = await import("./privacy-layer"); - + // 1. Detect persona if auto const personaId = req.personaId || "auto"; let detectedPersona: PersonaId = personaId; diff --git a/src/lib/privacy-layer.ts b/src/lib/privacy-layer.ts index 6431e37..0bcf975 100644 --- a/src/lib/privacy-layer.ts +++ b/src/lib/privacy-layer.ts @@ -1,7 +1,5 @@ export type PrivacyLevel = "MAXIMUM" | "HIGH" | "MEDIUM" | "STANDARD" | "LOW"; -// NOTE: SENSITIVITY_THRESHOLDS is also exported from this file for backward compatibility. -// The canonical source is src/lib/constants.ts — keep these in sync. export const SENSITIVITY_THRESHOLDS = { MAXIMUM: 75, HIGH: 55, @@ -33,9 +31,18 @@ export interface PrivacyStep { status: "completed" | "processing" | "pending"; } +export interface PiiEntity { + type: string; + value: string; + start: number; + end: number; + score: number; +} + export interface SensitivityAnalysis { category: string; keywords: string[]; + piiEntities: PiiEntity[]; sensitivityScore: number; // 0-100 recommendedLevel: PrivacyLevel; reasons: string[]; @@ -47,7 +54,7 @@ const SENSITIVITY_KEYWORDS: Record 19) return false; + + let sum = 0; + let isEven = false; + for (let i = digits.length - 1; i >= 0; i--) { + let d = parseInt(digits.charAt(i), 10); + if (isEven) { + d *= 2; + if (d > 9) d -= 9; + } + sum += d; + isEven = !isEven; + } + return sum % 10 === 0; +} + /** * Analyzes the sensitivity of input text and recommends a privacy level. * - * Uses keyword matching across 11 categories (medical, financial, legal, etc.) - * with weighted scoring to determine sensitivity score (0-100). - * - * @param request - The user's request/query text - * @param data - Optional additional data to analyze - * @returns Sensitivity analysis with score, keywords, and recommended level - * - * @example - * ```typescript - * const analysis = analyzeSensitivity( - * "Analyze this patient record", - * "Patient ID: 12345, Diagnosis: Diabetes" - * ); - * console.log(analysis.sensitivityScore); // 85 - * console.log(analysis.recommendedLevel); // "MAXIMUM" - * ``` + * Incorporates microsoft/presidio style PII detection architectures + * alongside traditional weighted keyword matching. */ function analyzeSensitivity(request: string, data?: string): SensitivityAnalysis { - const text = `${request} ${data || ""}`.toLowerCase(); + const text = `${request} ${data || ""}`; + const lowerText = text.toLowerCase(); let totalScore = 0; const matchedCategories: string[] = []; const matchedKeywords: string[] = []; const reasons: string[] = []; + const piiEntities: PiiEntity[] = []; + + // --- Presidio-style PII Detection --- + + // 1. Email Address Recognizer + const emailRegex = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g; + let match; + while ((match = emailRegex.exec(text)) !== null) { + piiEntities.push({ type: "EMAIL_ADDRESS", value: match[0], start: match.index, end: match.index + match[0].length, score: 1.0 }); + totalScore += 25; + } + + // 2. Phone Number Recognizer (US/Intl basic) + const phoneRegex = /\b(?:\+?1[-. ]?)?\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})\b/g; + while ((match = phoneRegex.exec(text)) !== null) { + piiEntities.push({ type: "PHONE_NUMBER", value: match[0], start: match.index, end: match.index + match[0].length, score: 0.85 }); + totalScore += 20; + } + + // 3. US SSN Recognizer (with boundary constraints) + const ssnRegex = /\b(?!000|666)[0-8][0-9]{2}[- ]?(?!00)[0-9]{2}[- ]?(?!0000)[0-9]{4}\b/g; + while ((match = ssnRegex.exec(text)) !== null) { + piiEntities.push({ type: "US_SSN", value: match[0], start: match.index, end: match.index + match[0].length, score: 0.95 }); + totalScore += 50; + } + + // 4. Credit Card Recognizer (Regex + Luhn checksum validation) + const ccRegex = /\b(?:\d[ -]*?){13,19}\b/g; + while ((match = ccRegex.exec(text)) !== null) { + if (luhnCheck(match[0])) { + piiEntities.push({ type: "CREDIT_CARD", value: match[0], start: match.index, end: match.index + match[0].length, score: 1.0 }); + totalScore += 60; + } + } + if (piiEntities.length > 0) { + const types = Array.from(new Set(piiEntities.map(e => e.type))).join(", "); + reasons.push(`Detected ${piiEntities.length} PII entity/entities (${types}) via robust pattern matching.`); + matchedCategories.push("personal_identity"); + } + + // --- Keyword Matching (Fallback & Context) --- for (const [category, config] of Object.entries(SENSITIVITY_KEYWORDS)) { let categoryMatches = 0; const categoryKeywords: string[] = []; for (const keyword of config.keywords) { - if (text.includes(keyword.toLowerCase())) { + if (lowerText.includes(keyword.toLowerCase())) { categoryMatches++; categoryKeywords.push(keyword); } @@ -129,14 +186,16 @@ function analyzeSensitivity(request: string, data?: string): SensitivityAnalysis if (categoryMatches > 0) { totalScore += config.weight + (categoryMatches * 3); - matchedCategories.push(category); + if (!matchedCategories.includes(category)) { + matchedCategories.push(category); + } matchedKeywords.push(...categoryKeywords); const categoryNames: Record = { medical: "Medical/health information detected", financial: "Financial/banking data detected", legal: "Legal/attorney-privileged content detected", - personal_identity: "Personal identity information (PII) detected", + personal_identity: "Personal identity keywords detected", government: "Government/classified information detected", business_strategy: "Business strategy/internal documents detected", credentials: "Credentials/secrets detected", @@ -145,11 +204,15 @@ function analyzeSensitivity(request: string, data?: string): SensitivityAnalysis analytics: "Analytics/aggregate data detected", public: "Public/general information detected" }; - reasons.push(categoryNames[category] || `${category} detected`); + + // Prevent duplicate reasoning if PII engine already flagged it + if (!(category === "personal_identity" && piiEntities.length > 0)) { + reasons.push(categoryNames[category] || `${category} detected`); + } } } - // Length heuristic - longer documents with company data might be sensitive + // Length heuristic - longer documents might hide sensitive data if ((data?.length || 0) > 2000) { totalScore += 5; reasons.push("Large document detected - increased scrutiny applied"); @@ -174,37 +237,22 @@ function analyzeSensitivity(request: string, data?: string): SensitivityAnalysis // Determine primary category let primaryCategory = matchedCategories[0] || "general"; - if (matchedCategories.includes("medical") || matchedCategories.includes("government")) { - primaryCategory = matchedCategories.find(c => c === "medical" || c === "government") || primaryCategory; + if (matchedCategories.includes("medical") || matchedCategories.includes("government") || piiEntities.length > 0) { + primaryCategory = matchedCategories.find(c => c === "medical" || c === "government" || c === "personal_identity") || primaryCategory; } return { category: primaryCategory, keywords: Array.from(new Set(matchedKeywords)), + piiEntities, sensitivityScore: totalScore, recommendedLevel, - reasons + reasons: Array.from(new Set(reasons)) }; } /** * Gets the privacy method details for a given privacy level. - * - * Returns comprehensive information about the privacy method including: - * - Processing method and description - * - Speed and quality characteristics - * - Security guarantees - * - Step-by-step processing pipeline - * - * @param level - The privacy level (MAXIMUM, HIGH, MEDIUM, STANDARD, LOW) - * @returns Complete privacy method details - * - * @example - * ```typescript - * const result = getPrivacyResult("MAXIMUM"); - * console.log(result.method); // "Local Model" - * console.log(result.dataNeverLeavesDevice); // true - * ``` */ function getPrivacyResult(level: PrivacyLevel): PrivacyResult { const results: Record = { @@ -329,41 +377,9 @@ export interface ProcessResponse { /** * Main function for privacy-aware processing. - * - * Analyzes input sensitivity, selects appropriate privacy method, - * and returns simulated processing results. - * - * **Input Validation:** - * - Request must be non-empty string (max 10,000 chars) - * - Data must be string (max 100,000 chars) if provided - * - * **Processing Steps:** - * 1. Validate inputs - * 2. Analyze sensitivity - * 3. Determine privacy level (or use override) - * 4. Get privacy method details - * 5. Simulate AI processing - * - * @param req - Processing request with text and optional override - * @returns Processing response with analysis, privacy method, and result - * @throws Error if validation fails or processing errors occur - * - * @example - * ```typescript - * const result = await processWithPrivacy({ - * request: "Analyze this patient medical record", - * data: "Patient ID: 12345\nDiagnosis: Type 2 Diabetes...", - * overrideLevel: "MAXIMUM" // Optional: force specific level - * }); - * - * console.log(result.privacy.level); // "MAXIMUM" - * console.log(result.analysis.sensitivityScore); // 85 - * console.log(result.simulatedResult); // AI response - * ``` */ export async function processWithPrivacy(req: ProcessRequest): Promise { try { - // Input validation — check type first, then emptiness if (req.request === null || req.request === undefined || typeof req.request !== "string") { throw new Error("Request must be a non-empty string"); } @@ -380,19 +396,11 @@ export async function processWithPrivacy(req: ProcessRequest): Promise setTimeout(resolve, 600)); return {