From 9334f88bf80b4add703e52bcbb1cb6aaef07a6a7 Mon Sep 17 00:00:00 2001 From: Aaditesh2307 Date: Sat, 16 Aug 2025 17:32:39 +0530 Subject: [PATCH] Authenticator Real Time Sensitive Data Redaction (On Device) --- Authenticator/Authenticator.crx | Bin 45486 -> 78336 bytes Authenticator/repo/background.js | 460 +++++- Authenticator/repo/content.js | 942 +++++++++++- Authenticator/repo/manifest.json | 30 +- Authenticator/repo/public/libs/xregexp.js | 24 + .../repo/public/ptr/apiPatterns_new.js | 0 Authenticator/repo/public/ptr/apipatterns.js | 1257 +++++++++++++++++ .../repo/public/ptr/cryptoPatterns.js | 118 ++ .../repo/public/ptr/cryptoPatterns_new.js | 0 Authenticator/repo/public/ptr/fiPatterns.js | 187 +++ .../repo/public/ptr/fiPatterns_new.js | 0 Authenticator/repo/public/ptr/medPatterns.js | 243 ++++ .../repo/public/ptr/medPatterns_new.js | 0 .../repo/public/ptr/networkPatterns.js | 110 ++ .../repo/public/ptr/networkPatterns_new.js | 0 Authenticator/repo/public/ptr/piiPatterns.js | 39 + extensions.json | 60 +- 17 files changed, 3415 insertions(+), 55 deletions(-) create mode 100644 Authenticator/repo/public/libs/xregexp.js create mode 100644 Authenticator/repo/public/ptr/apiPatterns_new.js create mode 100644 Authenticator/repo/public/ptr/apipatterns.js create mode 100644 Authenticator/repo/public/ptr/cryptoPatterns.js create mode 100644 Authenticator/repo/public/ptr/cryptoPatterns_new.js create mode 100644 Authenticator/repo/public/ptr/fiPatterns.js create mode 100644 Authenticator/repo/public/ptr/fiPatterns_new.js create mode 100644 Authenticator/repo/public/ptr/medPatterns.js create mode 100644 Authenticator/repo/public/ptr/medPatterns_new.js create mode 100644 Authenticator/repo/public/ptr/networkPatterns.js create mode 100644 Authenticator/repo/public/ptr/networkPatterns_new.js create mode 100644 Authenticator/repo/public/ptr/piiPatterns.js diff --git a/Authenticator/Authenticator.crx b/Authenticator/Authenticator.crx index a0b6a91a68409a6803af71a985df453c55f1b6ce..12319eb806387e44f5fa4443b029fb2a1ab97d1c 100644 GIT binary patch delta 43039 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 k|Lft$XErkb6O7M1|3`rZ3-_OEw13AM8UkXw`aj { + callback(result.enabledRedactionCategories || defaultRedactionCategories); + }); +} + +// Smart redaction function that preserves some context +function smartRedact(text, evidence, type) { + const len = evidence.length; + + // Special handling for different data types + switch (type.toLowerCase()) { + case 'email address': + return redactEmail(evidence); + + case 'phone number': + return redactPhoneNumber(evidence); + + case 'credit card': + case 'visa card number': + case 'mastercard number': + case 'american express (amex) number': + case 'discover card number': + case 'jcb card number': + case 'diners club card number': + case 'unionpay card number': + case 'maestro card number': + return redactCreditCard(evidence); + + case 'social security number (ssn)': + return redactSSN(evidence); + + // API Keys and Tokens + case 'openai user api key': + case 'google api key': + case 'github personal access token (classic)': + case 'github personal access token (fine-grained)': + case 'github oauth 2.0 access token': + case 'github user-to-server access token': + case 'github server-to-server access token': + case 'github refresh token': + case 'aws access id key': + case 'aws secret key': + case 'stripe api key': + case 'slack api token': + case 'discord bot token': + case 'telegram bot token': + return redactAPIKey(evidence); + + // Cryptocurrency + case 'bitcoin (btc)': + case 'ethereum (eth)': + case 'cardano (ada)': + case 'xrp (ripple)': + case 'solana (sol)': + case 'dogecoin (doge)': + case 'litecoin (ltc)': + return redactCryptoAddress(evidence); + + // Private Keys and Secrets + case 'bitcoin private key (wif)': + case 'ethereum private key': + case 'ripple secret key': + case 'solana private key': + case 'stellar secret key': + return redactPrivateKey(evidence); + + default: + // For unrecognized types, apply generic redaction + return redactGeneric(evidence); + } +} + +// Redact email addresses (show prefix and domain) +function redactEmail(email) { + const emailParts = email.split('@'); + if (emailParts.length === 2) { + const prefix = emailParts[0]; + const domain = emailParts[1]; + + const visiblePrefix = prefix.length > 3 ? prefix.substring(0, 2) : prefix.substring(0, 1); + const prefixRedacted = visiblePrefix + REDACTION_CONFIG.REDACTION_CHAR.repeat(Math.max(1, prefix.length - visiblePrefix.length)); + + return `${prefixRedacted}@${domain}`; + } + return redactGeneric(email); +} + +// Redact phone numbers (show area code) +function redactPhoneNumber(phone) { + const cleanPhone = phone.replace(/\D/g, ''); + if (cleanPhone.length >= 10) { + const areaCode = cleanPhone.substring(0, 3); + const redactedDigits = areaCode + REDACTION_CONFIG.REDACTION_CHAR.repeat(cleanPhone.length - 3); + + // Replace each digit in the original string with corresponding redacted digit + let redactedPhone = phone; + let digitIndex = 0; + + for (let i = 0; i < phone.length; i++) { + if (/\d/.test(phone[i])) { + redactedPhone = redactedPhone.substring(0, i) + redactedDigits[digitIndex] + redactedPhone.substring(i + 1); + digitIndex++; + } + } + + return redactedPhone; + } + return redactGeneric(phone); +} + +// Redact credit card numbers (show last 4 digits) +function redactCreditCard(cardNumber) { + const cleanCard = cardNumber.replace(/\D/g, ''); + if (cleanCard.length >= 12) { + const last4 = cleanCard.slice(-4); + const redactedDigits = REDACTION_CONFIG.REDACTION_CHAR.repeat(cleanCard.length - 4) + last4; + + // Replace each digit in the original string with corresponding redacted digit + let redactedCard = cardNumber; + let digitIndex = 0; + + for (let i = 0; i < cardNumber.length; i++) { + if (/\d/.test(cardNumber[i])) { + redactedCard = redactedCard.substring(0, i) + redactedDigits[digitIndex] + redactedCard.substring(i + 1); + digitIndex++; + } + } + + return redactedCard; + } + return redactGeneric(cardNumber); +} + +// Redact SSN (show last 4 digits) +function redactSSN(ssn) { + const cleanSSN = ssn.replace(/\D/g, ''); + if (cleanSSN.length === 9) { + const last4 = cleanSSN.slice(-4); + const redactedDigits = REDACTION_CONFIG.REDACTION_CHAR.repeat(5) + last4; + + // Replace each digit in the original string with corresponding redacted digit + let redactedSSN = ssn; + let digitIndex = 0; + + for (let i = 0; i < ssn.length; i++) { + if (/\d/.test(ssn[i])) { + redactedSSN = redactedSSN.substring(0, i) + redactedDigits[digitIndex] + redactedSSN.substring(i + 1); + digitIndex++; + } + } + + return redactedSSN; + } + return redactGeneric(ssn); +} + +// Generic redaction (show first 2-4 characters) +function redactGeneric(text) { + const len = text.length; + if (len <= 4) { + return REDACTION_CONFIG.REDACTION_CHAR.repeat(len); + } + + const revealLength = Math.min(REDACTION_CONFIG.MAX_REVEAL_LENGTH, Math.max(REDACTION_CONFIG.MIN_REVEAL_LENGTH, Math.floor(len * 0.2))); + const visible = text.substring(0, revealLength); + const redacted = REDACTION_CONFIG.REDACTION_CHAR.repeat(len - revealLength); + + return visible + redacted; +} + +// Redact API Keys (show prefix, hide most of the key) +function redactAPIKey(apiKey) { + const len = apiKey.length; + + // For very short keys, redact completely + if (len <= 6) { + return REDACTION_CONFIG.REDACTION_CHAR.repeat(len); + } + + // For keys with common prefixes (sk-, ghp_, AKIA, etc.), show prefix + few chars + const prefixes = ['sk-', 'ghp_', 'gho_', 'ghu_', 'ghs_', 'ghr_', 'AKIA', 'AIza', 'ya29.', 'xoxb-', 'xoxa-', 'xoxp-', 'xoxr-', 'xoxs-']; + + for (const prefix of prefixes) { + if (apiKey.startsWith(prefix)) { + const visibleLength = Math.min(prefix.length + 2, len - 6); // Show prefix + 2 chars, but leave at least 6 chars to redact + const visible = apiKey.substring(0, visibleLength); + const redacted = REDACTION_CONFIG.REDACTION_CHAR.repeat(len - visibleLength); + return visible + redacted; + } + } + + // For keys without recognizable prefixes, show first 3-4 characters + const revealLength = Math.min(4, Math.max(2, Math.floor(len * 0.15))); + const visible = apiKey.substring(0, revealLength); + const redacted = REDACTION_CONFIG.REDACTION_CHAR.repeat(len - revealLength); + + return visible + redacted; +} + +// Redact cryptocurrency addresses (show first few and last few chars) +function redactCryptoAddress(address) { + const len = address.length; + + if (len <= 8) { + return REDACTION_CONFIG.REDACTION_CHAR.repeat(len); + } + + // Show first 4 and last 4 characters for addresses + const firstPart = address.substring(0, 4); + const lastPart = address.substring(len - 4); + const redacted = REDACTION_CONFIG.REDACTION_CHAR.repeat(len - 8); + + return firstPart + redacted + lastPart; +} + +// Redact private keys and secrets (almost complete redaction) +function redactPrivateKey(privateKey) { + const len = privateKey.length; + + if (len <= 4) { + return REDACTION_CONFIG.REDACTION_CHAR.repeat(len); + } + + // For private keys, show only first 2 characters for identification + const visible = privateKey.substring(0, 2); + const redacted = REDACTION_CONFIG.REDACTION_CHAR.repeat(len - 2); + + return visible + redacted; +} + +// Main function to identify and redact sensitive data +function identifyAndRedactSensitiveData(text, callback) { + console.log("🔍 Starting text analysis for redaction..."); + + getEnabledRedactionCategories((enabledCategories) => { + let sensitiveDataFound = []; + let redactedText = text; + + // Iterate through each enabled category + Object.keys(allRedactionPatterns).forEach(categoryKey => { + if (enabledCategories[categoryKey] === true) { + const categoryPatterns = allRedactionPatterns[categoryKey]; + + // Iterate through patterns in the category + Object.keys(categoryPatterns).forEach(patternKey => { + const patternObj = categoryPatterns[patternKey]; + const regex = patternObj.pattern; + + let match; + // Reset regex lastIndex for global patterns + regex.lastIndex = 0; + + while ((match = regex.exec(text)) !== null) { + const evidence = match[0]; + + // Skip if this evidence has already been processed + if (sensitiveDataFound.some(item => item.evidence === evidence && item.startIndex === match.index)) { + if (!regex.global) break; + continue; + } + + const redactedEvidence = smartRedact(text, evidence, patternObj.type); + + sensitiveDataFound.push({ + category: categoryKey, + type: patternObj.type, + pattern: regex.toString(), + evidence: evidence, + redactedEvidence: redactedEvidence, + startIndex: match.index, + endIndex: match.index + evidence.length, + tags: patternObj.tags || [] + }); + + // Replace the original text with redacted version + redactedText = redactedText.replace(evidence, redactedEvidence); + + console.log(`🎭 Found and redacted ${patternObj.type}: ${evidence} -> ${redactedEvidence}`); + + // Prevent infinite loop for non-global regexes + if (!regex.global) break; + } + }); + } + }); + + console.log(`🔍 Analysis complete. Found ${sensitiveDataFound.length} sensitive items.`); + + callback({ + originalText: text, + redactedText: redactedText, + sensitiveItems: sensitiveDataFound, + hasSensitiveData: sensitiveDataFound.length > 0 + }); + }); +} + // Configuration for content analysis const CONFIG = { GEMINI_API_BASE: "https://generativelanguage.googleapis.com/v1beta/models/", @@ -238,7 +613,7 @@ class ContentAnalyzer { const truncatedPrompt = prompt.length > CONFIG.MAX_PROMPT_LENGTH ? prompt.substring(0, CONFIG.MAX_PROMPT_LENGTH) + - "\n\n[Content truncated for length]" + "\n\n[Content truncated for length]" : prompt; console.log("📤 Sending analysis request to Gemini..."); @@ -479,8 +854,7 @@ class ContentAnalyzer { } else { console.log("✅ Masking successful:", response); console.log( - `🎭 Masked ${response.masked || 0} elements out of ${ - cssSelectors.length + `🎭 Masked ${response.masked || 0} elements out of ${cssSelectors.length } selectors` ); resolve({ success: true, response: response }); @@ -513,6 +887,76 @@ class ContentAnalyzer { chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { console.log("Background received message:", message); + // ====================== + // AI WEBSITE TEXT REDACTION HANDLING (NEW) + // ====================== + + // Handle AI website send button interception and text analysis + if (message.type === "ANALYZE_AI_TEXT") { + console.log("🤖 Received AI text analysis request from tab:", sender.tab?.id); + + const { textData, website, inputType } = message; + + if (!textData || textData.trim().length === 0) { + sendResponse({ + success: false, + error: "No text provided for analysis" + }); + return true; + } + + console.log(`🔍 Analyzing text from ${website} (${inputType}):`, textData.substring(0, 100) + "..."); + + identifyAndRedactSensitiveData(textData, (result) => { + const response = { + success: true, + originalText: result.originalText, + redactedText: result.redactedText, + hasSensitiveData: result.hasSensitiveData, + sensitiveItems: result.sensitiveItems, + website: website, + inputType: inputType, + timestamp: Date.now() + }; + + console.log(`✅ Analysis complete for ${website}:`, { + hasSensitiveData: response.hasSensitiveData, + itemCount: response.sensitiveItems.length, + categories: [...new Set(response.sensitiveItems.map(item => item.category))] + }); + + sendResponse(response); + }); + + return true; // Keep message channel open for async response + } + + // Handle getting redaction settings + if (message.type === "GET_REDACTION_SETTINGS") { + chrome.storage.sync.get(['enabledRedactionCategories', 'redactionEnabled'], (result) => { + sendResponse({ + success: true, + enabledCategories: result.enabledRedactionCategories || defaultRedactionCategories, + redactionEnabled: result.redactionEnabled !== false + }); + }); + return true; + } + + // Handle updating redaction settings + if (message.type === "UPDATE_REDACTION_SETTINGS") { + const { enabledCategories, redactionEnabled } = message; + + chrome.storage.sync.set({ + enabledRedactionCategories: enabledCategories, + redactionEnabled: redactionEnabled + }, () => { + sendResponse({ success: true }); + }); + + return true; + } + // Handle SAML response processing (existing functionality) if (message.action === "processSamlResponse") { console.log("Background: Processing SAML response"); @@ -679,4 +1123,14 @@ chrome.tabs.onRemoved.addListener((tabId) => { } }); +// ====================== +// REDACTION INITIALIZATION +// ====================== + +// Initialize redaction settings on extension install/startup +chrome.runtime.onInstalled.addListener(() => { + console.log("🔧 Initializing redaction settings..."); + initializeRedactionSettings(); +}); + console.log("Background script loaded successfully"); diff --git a/Authenticator/repo/content.js b/Authenticator/repo/content.js index 4a3d22fdc..6d71fee2f 100644 --- a/Authenticator/repo/content.js +++ b/Authenticator/repo/content.js @@ -12,6 +12,70 @@ let analysisPerformed = false; let currentUrl = window.location.href; + // Track pending API calls and send button blocking + let pendingAPICallsCount = 0; + const blockedSendButtons = new Set(); + + // Add bypass flag to prevent infinite loops in send button interception + let bypassSendInterception = false; // AI Website Send Button Configurations + const AI_WEBSITES = { + 'chatgpt.com': { + selectors: [ + 'button[data-testid="send-button"]', + 'button[aria-label="Send message"]', + '[data-testid="send-button"]', + 'button.composer-submit-btn', + 'button[type="submit"]' + ] + }, + 'openai.com': { + selectors: [ + 'button[data-testid="send-button"]', + 'button[aria-label="Send message"]', + '[data-testid="send-button"]', + 'button.composer-submit-btn' + ] + }, + 'gemini.google.com': { + selectors: [ + 'button[aria-label="Send message"]', + 'button[data-testid="send-button"]', + 'button[jsname="M2UYVd"]', + 'button.send-button', + 'mat-icon[fonticon="send"]', + 'button[mat-ripple]' + ] + }, + 'bard.google.com': { + selectors: [ + 'button[aria-label="Send message"]', + 'button[data-testid="send-button"]', + 'button.send-button' + ] + }, + 'perplexity.ai': { + selectors: [ + 'button[data-testid="submit-button"]', + 'button[aria-label="Submit"]', + 'button.bg-super', + 'button[type="submit"]' + ] + }, + 'claude.ai': { + selectors: [ + 'button[aria-label="Send Message"]', + 'button[data-testid="send-button"]', + 'button.send-button' + ] + }, + 'poe.com': { + selectors: [ + 'button[class*="ChatMessageSendButton"]', + 'button[aria-label="Send"]' + ] + } + }; + // Check if we're in an extension context if (typeof chrome !== "undefined" && chrome.runtime && chrome.runtime.id) { console.log("Running in extension context"); @@ -83,7 +147,335 @@ } // ====================== - // CONTENT ANALYSIS FUNCTIONALITY (NEW) + // INPUT MONITORING & MASKING FUNCTIONALITY (NEW) + // ====================== + + // Configuration for input monitoring + const INPUT_CONFIG = { + ANALYSIS_DELAY: 2000, // Wait 2 seconds after user stops typing (increased to reduce API calls) + MIN_TEXT_LENGTH: 5, // Minimum text length to analyze (increased to reduce noise) + MASK_CHAR: "*" + // Removed SENSITIVE_PATTERNS - now using Gemini for all analysis + }; + + // Track input elements and their timers + const inputTracker = new Map(); + const analysisTimers = new Map(); + + // Function to check if text should be analyzed (simplified - let Gemini decide) + function shouldAnalyzeText(text) { + console.log("🔍 Preparing text for Gemini analysis:", text.substring(0, 30) + "..."); + return true; // Always analyze with Gemini, no pre-filtering + } + + // Helper function to add event listeners to a single element + function addEventListenersToElement(element) { + try { + // Skip if already has listeners + if (element._hasInputMonitoring) { + return; + } + + console.log("🎯 Element prepared for monitoring:", element.tagName, element.id || 'no-id'); + + // Keep other events for compatibility (but disable timer-based analysis) + const events = ['input', 'textInput', 'keyup', 'change']; + events.forEach(eventType => { + element.addEventListener(eventType, handleInputEvent, { passive: true }); + }); + element.addEventListener('paste', handlePasteEvent, { passive: true }); + + // Mobile touch events + element.addEventListener('touchend', () => { + setTimeout(() => handleInputEvent({ target: element }), 100); + }, { passive: true }); + + // Mark element as having listeners + element._hasInputMonitoring = true; + + console.log("🎯 Added listeners to element:", element.tagName, element.type || 'text'); + } catch (error) { + console.error("Error adding listeners to element:", error); + } + } + + // Function to analyze text using local pattern matching via background script + // Updated to use the new redaction functionality in background.js + async function analyzeTextWithGemini(text, element) { + console.log("📤 ===== ANALYZING WITH BACKGROUND REDACTION SERVICE ====="); + console.log("📤 Text to analyze:", text.substring(0, 100) + (text.length > 100 ? '...' : '')); + console.log("📤 Using background script redaction instead of API"); + + try { + const analysisData = { + text: text, + elementType: element.tagName.toLowerCase(), + elementName: element.name || '', + elementId: element.id || '', + elementClass: element.className || '', + placeholder: element.placeholder || '', + context: element.form ? 'form' : 'standalone' + }; + + console.log("📤 Analysis data:", analysisData); + + return new Promise((resolve) => { + // Send text to background script for redaction analysis + const messageData = { + type: "ANALYZE_AI_TEXT", + textData: text, + website: window.location.hostname, + inputType: analysisData.elementType + }; + + console.log("📤 Message being sent to background:", messageData); + + chrome.runtime.sendMessage(messageData, (response) => { + console.log("📥 ===== RESPONSE FROM BACKGROUND ====="); + console.log("📥 Chrome.runtime.lastError:", chrome.runtime.lastError); + console.log("📥 Response received:", response); + + if (chrome.runtime.lastError) { + console.error("❌ Chrome runtime error:", chrome.runtime.lastError); + resolve({ isSensitive: false, maskedText: text, error: chrome.runtime.lastError.message }); + } else if (response && response.success) { + console.log("✅ Redaction analysis completed successfully"); + resolve({ + isSensitive: response.hasSensitiveData, + maskedText: response.redactedText, + originalText: response.originalText, + sensitiveItems: response.sensitiveItems || [], + redactionApplied: response.hasSensitiveData, + detectedTypes: response.sensitiveItems.map(item => item.type) || [] + }); + } else { + console.log("⚠️ Background script returned unsuccessful response"); + resolve({ isSensitive: false, maskedText: text, error: response?.error || 'Unknown error' }); + } + }); + }); + } catch (error) { + console.error("❌ Error in text analysis:", error); + console.error("❌ Error stack:", error.stack); + return { isSensitive: false, maskedText: text, error: error.message }; + } + } + + // Function to handle input events + function handleInputEvent(event) { + const element = event.target; + const text = element.value || element.textContent || element.innerText || ''; + + console.log(`🔍 Input event detected:`, { + text: text.substring(0, 20) + (text.length > 20 ? '...' : ''), + length: text.length, + element: element.tagName, + type: element.type || 'none', + id: element.id || 'no-id' + }); + + // Skip if text is too short or element is not relevant + if (!text || text.length < INPUT_CONFIG.MIN_TEXT_LENGTH) { + console.log("⏩ Skipping - text too short or empty"); + return; + } + + // Skip if this is a password field (already handled by browser) + if (element.type === 'password') { + console.log("⏩ Skipping - password field"); + return; + } + + // Clear existing timer for this element + const elementId = element.id || element.tagName + '_' + Math.random().toString(36).substr(2, 9); + if (analysisTimers.has(elementId)) { + clearTimeout(analysisTimers.get(elementId)); + console.log("⏱️ Cleared existing timer for element"); + } + + // Set new timer for delayed analysis + // Analysis now happens on input events or send button click + console.log("💡 Analysis will trigger on input events or when send button is clicked"); + } + + // Function to handle paste events + function handlePasteEvent(event) { + const element = event.target; + + // Skip password fields + if (element.type === 'password') { + return; + } + + setTimeout(async () => { + const text = element.value; + + if (text && text.length >= INPUT_CONFIG.MIN_TEXT_LENGTH) { + console.log("📋 Analyzing pasted content with Gemini..."); + + // Send directly to Gemini for analysis (no pre-filtering) + try { + const analysis = await analyzeTextWithGemini(text, element); + console.log("🔍 Gemini paste analysis result:", analysis); + + if (analysis.isSensitive && analysis.maskedText && analysis.maskedText !== text) { + element.value = analysis.maskedText; + console.log("🎭 Applied Gemini-powered masking to pasted content"); + element.dispatchEvent(new Event('input', { bubbles: true })); + } else { + console.log("✅ Gemini: No sensitive data detected in paste"); + } + } catch (error) { + console.error("❌ Error in paste analysis:", error); + } + } + }, 50); // Small delay to ensure paste content is available + } + + // Function to add event listeners to input elements + function addInputMonitoring() { + // Ensure document is available + if (!document || !document.body) { + console.log("⏳ Document not ready, skipping input monitoring"); + return; + } + + // Enhanced selectors for mobile compatibility + const inputSelectors = [ + 'input[type="text"]', + 'input[type="email"]', + 'input[type="tel"]', + 'input[type="number"]', + 'input[type="search"]', + 'input:not([type])', // Default inputs + 'textarea', + '[contenteditable="true"]', + '[contenteditable=""]', // Empty contenteditable + 'div[role="textbox"]', // ARIA textboxes + '[data-testid*="input"]', // Common test IDs + '[data-cy*="input"]', + '.input', // Common class names + '.textbox', + '.text-input' + ]; + + const inputElements = document.querySelectorAll(inputSelectors.join(', ')); + + console.log(`🔍 Found ${inputElements.length} input elements to monitor`); + + inputElements.forEach((element, index) => { + try { + // Use the helper function to add listeners + addEventListenersToElement(element); + + console.log(`🎯 Processed element ${index + 1}:`, { + tag: element.tagName, + type: element.type || 'text', + id: element.id || 'no-id', + className: element.className || 'no-class', + placeholder: element.placeholder || 'no-placeholder' + }); + } catch (error) { + console.error(`Error processing element ${index}:`, error); + } + }); // Also monitor the specific ChatGPT input if we're on ChatGPT + if (window.location.hostname.includes('chatgpt') || window.location.hostname.includes('openai')) { + console.log("🤖 ChatGPT detected, adding specific monitoring..."); + + // Try to find ChatGPT's input element with various selectors + const chatGptSelectors = [ + '#prompt-textarea', + '[data-id="root"]', + 'textarea[placeholder*="Message"]', + 'div[contenteditable="true"]', + '[role="textbox"]' + ]; + + chatGptSelectors.forEach(selector => { + const elements = document.querySelectorAll(selector); + elements.forEach(element => { + console.log(`🎯 Adding ChatGPT-specific monitoring to:`, selector); + const events = ['input', 'textInput', 'keyup', 'change', 'paste']; + events.forEach(eventType => { + element.addEventListener(eventType, handleInputEvent, { passive: true }); + }); + element.addEventListener('paste', handlePasteEvent, { passive: true }); + }); + }); + } + } + + // Function to monitor for new input elements (for dynamic content) + function setupInputObserver() { + const observer = new MutationObserver((mutations) => { + let newElementsFound = false; + + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === 1) { // Element node + // Enhanced selectors for new elements + const inputSelectors = [ + 'input[type="text"]', 'input[type="email"]', 'input[type="tel"]', 'input[type="number"]', + 'input[type="search"]', 'input:not([type])', 'textarea', '[contenteditable="true"]', + '[contenteditable=""]', 'div[role="textbox"]', '[data-testid*="input"]' + ]; + + // Check if the added node itself is an input element + const isInputElement = inputSelectors.some(selector => { + try { + return node.matches && node.matches(selector); + } catch (e) { + return false; + } + }); + + if (isInputElement) { + console.log("🎯 Found new input element:", node.tagName, node.type || 'unknown'); + addEventListenersToElement(node); + newElementsFound = true; + } + + // Check for input elements within the added node + if (node.querySelectorAll) { + const newInputs = node.querySelectorAll(inputSelectors.join(', ')); + if (newInputs.length > 0) { + console.log(`🎯 Found ${newInputs.length} nested input elements`); + newInputs.forEach(input => { + addEventListenersToElement(input); + }); + newElementsFound = true; + } + } + } + }); + }); + + if (newElementsFound) { + console.log("✅ Added monitoring to new dynamic elements"); + } + }); + + // Ensure document.body exists before observing + if (document.body) { + observer.observe(document.body, { + childList: true, + subtree: true + }); + } else { + // Wait for document.body to be available + document.addEventListener('DOMContentLoaded', () => { + if (document.body) { + observer.observe(document.body, { + childList: true, + subtree: true + }); + } + }); + } + + return observer; + } // ====================== + // CONTENT ANALYSIS FUNCTIONALITY (EXISTING) // ====================== // Function to extract entire DOM content @@ -223,6 +615,541 @@ checkForExistingSamlResponses(); setTimeout(checkForExistingSamlResponses, 1000); + // Initialize input monitoring with multiple attempts for mobile compatibility + console.log("🔧 Initializing input monitoring..."); + + // Add this new function to intercept send button clicks + function interceptSendButtonClick() { + const websiteConfig = getCurrentWebsiteConfig(); + if (!websiteConfig) { + return; + } + + console.log("🔗 Setting up send button click interception..."); + + websiteConfig.selectors.forEach(selector => { + try { + const buttons = document.querySelectorAll(selector); + buttons.forEach(button => { + if (!button._hasClickInterceptor) { + // Store original click handler if any + const originalOnClick = button.onclick; + + // Add click event listener with capture to intercept before other handlers + button.addEventListener('click', async function (event) { + // Check bypass flag to prevent infinite loops + if (bypassSendInterception) { + console.log("🔄 Bypassing send button interception"); + return; // Allow the click to proceed normally + } + + console.log("🚫 ===== SEND BUTTON INTERCEPTED ====="); + + // Prevent the original click from executing + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + + console.log("🔍 Analyzing all input fields before sending..."); + + // Find all input fields that might contain text + const inputSelectors = [ + 'input[type="text"]', 'input[type="email"]', 'input[type="tel"]', + 'input[type="number"]', 'input[type="search"]', 'input:not([type])', + 'textarea', '[contenteditable="true"]', '[contenteditable=""]', + 'div[role="textbox"]', '[data-testid*="input"]' + ]; + + const inputElements = document.querySelectorAll(inputSelectors.join(', ')); + const analysisPromises = []; + + // Analyze each input field that has text + inputElements.forEach(element => { + const text = element.value || element.textContent || element.innerText || ''; + + if (text && text.length >= INPUT_CONFIG.MIN_TEXT_LENGTH && element.type !== 'password') { + console.log("📝 Found text in element:", element.tagName, text.substring(0, 30)); + + // Increment pending API calls + incrementPendingAPICalls(); + + const analysisPromise = analyzeTextWithGemini(text, element) + .then(analysis => { + console.log("✅ Pre-send analysis complete for element"); + + if (analysis.isSensitive && analysis.maskedText && analysis.maskedText !== text) { + console.log("🎭 Applying redaction before send"); + console.log("🎭 Original text:", text.substring(0, 50) + "..."); + console.log("🎭 Masked text:", analysis.maskedText.substring(0, 50) + "..."); + + if (element.value !== undefined) { + element.value = analysis.maskedText; + } else if (element.textContent !== undefined) { + element.textContent = analysis.maskedText; + } else if (element.innerText !== undefined) { + element.innerText = analysis.maskedText; + } + + // Trigger input event to notify the application of the change + element.dispatchEvent(new Event('input', { bubbles: true })); + element.dispatchEvent(new Event('change', { bubbles: true })); + console.log("✅ Redaction applied before send"); + } else { + console.log("ℹ️ No redaction needed for this element"); + } + + return analysis; + }) + .finally(() => { + // Always decrement when done + decrementPendingAPICalls(); + }); + + analysisPromises.push(analysisPromise); + } + }); + + if (analysisPromises.length === 0) { + console.log("📝 No text found to analyze, proceeding with send"); + // If no analysis needed, trigger the original click with bypass + setTimeout(() => { + console.log("🚀 Triggering send with bypass flag"); + bypassSendInterception = true; + + if (originalOnClick) { + originalOnClick.call(button, event); + } else { + // Create and dispatch a new click event + const newEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + button.dispatchEvent(newEvent); + } + + // Reset bypass flag after a short delay + setTimeout(() => { + bypassSendInterception = false; + console.log("🔄 Reset bypass flag"); + }, 100); + }, 100); + return; + } + + console.log(`⏳ Waiting for ${analysisPromises.length} analysis operations to complete...`); + + try { + // Wait for all analyses to complete + await Promise.all(analysisPromises); + + console.log("✅ All pre-send analyses complete, proceeding with send"); + + // Small delay to ensure UI updates + setTimeout(() => { + console.log("🚀 Triggering original send action with bypass"); + bypassSendInterception = true; + + if (originalOnClick) { + originalOnClick.call(button, event); + } else { + // Create and dispatch a new click event + const newEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + button.dispatchEvent(newEvent); + } + + // Reset bypass flag after a short delay + setTimeout(() => { + bypassSendInterception = false; + console.log("🔄 Reset bypass flag after successful send"); + }, 100); + }, 500); // 500ms delay to ensure all updates are complete + + } catch (error) { + console.error("❌ Error during pre-send analysis:", error); + + // Still allow sending even if analysis fails + setTimeout(() => { + console.log("🚀 Triggering send despite analysis error"); + bypassSendInterception = true; + + if (originalOnClick) { + originalOnClick.call(button, event); + } else { + const newEvent = new MouseEvent('click', { + bubbles: true, + cancelable: true, + view: window + }); + button.dispatchEvent(newEvent); + } + + // Reset bypass flag after a short delay + setTimeout(() => { + bypassSendInterception = false; + console.log("🔄 Reset bypass flag after error recovery"); + }, 100); + }, 100); + } + + }, true); // Use capture phase + + button._hasClickInterceptor = true; + console.log("✅ Added click interceptor to send button"); + } + }); + } catch (error) { + console.log("❌ Error setting up send button interceptor:", error); + } + }); + } + + // Function to monitor for new send buttons (for dynamic content) + function setupSendButtonObserver() { + const websiteConfig = getCurrentWebsiteConfig(); + if (!websiteConfig) { + return; + } + + // Set up initial interceptors + interceptSendButtonClick(); + + const observer = new MutationObserver((mutations) => { + let newButtonsFound = false; + + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + // Check if the added node itself is a send button + websiteConfig.selectors.forEach(selector => { + try { + if (node.matches && node.matches(selector)) { + console.log("🔍 New send button detected:", selector); + newButtonsFound = true; + } + + // Check if the added node contains send buttons + const buttons = node.querySelectorAll ? node.querySelectorAll(selector) : []; + if (buttons.length > 0) { + console.log("🔍 New send buttons found in added content:", selector); + newButtonsFound = true; + } + } catch (error) { + // Ignore selector errors + } + }); + } + }); + }); + + if (newButtonsFound) { + // Set up interceptors for new buttons + setTimeout(() => { + interceptSendButtonClick(); + }, 100); + } + + // Only block if we have pending API calls + if (pendingAPICallsCount > 0) { + blockSendButtons(); + } + }); + + if (document.body) { + observer.observe(document.body, { + childList: true, + subtree: true + }); + console.log("👀 Send button observer started with click interception"); + } + + return observer; + } + + // Function to initialize all monitoring + function initializeMonitoring() { + // Initial setup + addInputMonitoring(); + + // Setup mutation observer for dynamic content (only if document.body exists) + if (document.body) { + setupInputObserver(); + + // Setup send button observer for AI websites + setupSendButtonObserver(); + } + } + + // Initialize monitoring when document is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeMonitoring); + } else { + initializeMonitoring(); + } + + // Re-run input monitoring after delays to catch late-loading elements + setTimeout(() => { + console.log("🔧 Re-running input monitoring after 2s..."); + addInputMonitoring(); + }, 2000); + + setTimeout(() => { + console.log("🔧 Re-running input monitoring after 5s..."); + addInputMonitoring(); + }, 5000); + + // Test function to verify input monitoring is working + function testInputMonitoring() { + try { + console.log("🧪 Testing input monitoring..."); + + if (!document) { + console.log("🧪 Document not available"); + return; + } + + const allInputs = document.querySelectorAll('input, textarea, [contenteditable]'); + console.log(`🧪 Found ${allInputs.length} total input-like elements on page`); + + allInputs.forEach((input, index) => { + console.log(`🧪 Input ${index + 1}:`, { + tag: input.tagName, + type: input.type || 'none', + id: input.id || 'no-id', + className: input.className || 'no-class', + placeholder: input.placeholder || 'no-placeholder', + hasEventListeners: input._hasInputMonitoring || false + }); + }); + } catch (error) { + console.error("🧪 Test function error:", error); + } + } + + // Run test after page loads + setTimeout(testInputMonitoring, 3000); + + // ====================== + // SEND BUTTON BLOCKING FUNCTIONALITY + // ====================== + + // Function to get current website configuration + function getCurrentWebsiteConfig() { + const hostname = window.location.hostname.toLowerCase(); + + for (const [domain, config] of Object.entries(AI_WEBSITES)) { + if (hostname.includes(domain)) { + console.log("🌐 Detected AI website:", domain); + return config; + } + } + + return null; + } + + // Function to find and block send buttons + function blockSendButtons() { + const websiteConfig = getCurrentWebsiteConfig(); + if (!websiteConfig) { + console.log("🌐 Not on a configured AI website, skipping send button blocking"); + return; + } + + console.log("🚫 ===== BLOCKING SEND BUTTONS ====="); + + let buttonsFound = 0; + websiteConfig.selectors.forEach(selector => { + try { + const buttons = document.querySelectorAll(selector); + buttons.forEach(button => { + if (!blockedSendButtons.has(button)) { + // Store original state + const originalDisabled = button.disabled; + const originalStyle = button.style.cssText; + const originalTitle = button.title; + + // Disable the button + button.disabled = true; + button.style.opacity = '0.5'; + button.style.cursor = 'not-allowed'; + button.title = 'Waiting for text redaction to complete...'; + + // Store button reference and original state + blockedSendButtons.add(button); + button._originalState = { + disabled: originalDisabled, + style: originalStyle, + title: originalTitle + }; + + buttonsFound++; + console.log("🚫 Blocked send button:", selector); + } + }); + } catch (error) { + console.log("🌐 Selector not found:", selector); + } + }); + + if (buttonsFound > 0) { + console.log(`🚫 Successfully blocked ${buttonsFound} send buttons`); + } else { + console.log("⚠️ No send buttons found to block"); + } + } + + // Function to unblock send buttons + function unblockSendButtons() { + console.log("✅ ===== UNBLOCKING SEND BUTTONS ====="); + + let buttonsUnblocked = 0; + blockedSendButtons.forEach(button => { + try { + if (button._originalState) { + // Restore original state + button.disabled = button._originalState.disabled; + button.style.cssText = button._originalState.style; + button.title = button._originalState.title; + + delete button._originalState; + buttonsUnblocked++; + } + } catch (error) { + console.log("🌐 Error unblocking button:", error); + } + }); + + // Clear the blocked buttons set + blockedSendButtons.clear(); + + if (buttonsUnblocked > 0) { + console.log(`✅ Successfully unblocked ${buttonsUnblocked} send buttons`); + } + } + + // Function to increment pending API calls and block buttons + function incrementPendingAPICalls() { + pendingAPICallsCount++; + console.log("📈 Pending API calls:", pendingAPICallsCount); + + if (pendingAPICallsCount === 1) { + // First API call - block send buttons + blockSendButtons(); + } + } + + // Function to decrement pending API calls and unblock when done + function decrementPendingAPICalls() { + pendingAPICallsCount = Math.max(0, pendingAPICallsCount - 1); + console.log("📉 Pending API calls:", pendingAPICallsCount); + + if (pendingAPICallsCount === 0) { + // All API calls complete - unblock send buttons + console.log("🎉 All API calls complete - unblocking send buttons"); + unblockSendButtons(); + } + } + + // Add test functions for send button blocking + window.testSendButtonBlocking = function () { + console.log("🧪 ===== TESTING SEND BUTTON BLOCKING ====="); + console.log("🧪 Current website:", window.location.hostname); + console.log("🧪 Pending API calls:", pendingAPICallsCount); + + // Simulate API call + incrementPendingAPICalls(); + + setTimeout(() => { + console.log("🧪 Simulating API completion..."); + decrementPendingAPICalls(); + }, 3000); + }; + + window.testMultipleAPICalls = function () { + console.log("🧪 ===== TESTING MULTIPLE API CALLS ====="); + + // Simulate 3 API calls + incrementPendingAPICalls(); + incrementPendingAPICalls(); + incrementPendingAPICalls(); + + console.log("🧪 Started 3 API calls, will complete them one by one..."); + + setTimeout(() => { + console.log("🧪 Completing API call 1/3"); + decrementPendingAPICalls(); + }, 2000); + + setTimeout(() => { + console.log("🧪 Completing API call 2/3"); + decrementPendingAPICalls(); + }, 4000); + + setTimeout(() => { + console.log("🧪 Completing API call 3/3"); + decrementPendingAPICalls(); + }, 6000); + }; + + window.getSendButtonStatus = function () { + console.log("📊 ===== SEND BUTTON STATUS ====="); + console.log("📊 Pending API calls:", pendingAPICallsCount); + console.log("📊 Blocked buttons count:", blockedSendButtons.size); + console.log("📊 Current website config:", getCurrentWebsiteConfig()); + + const websiteConfig = getCurrentWebsiteConfig(); + if (websiteConfig) { + websiteConfig.selectors.forEach(selector => { + const buttons = document.querySelectorAll(selector); + console.log(`📊 Found ${buttons.length} buttons for selector: ${selector}`); + buttons.forEach((button, index) => { + console.log(`📊 Button ${index + 1}: disabled=${button.disabled}, hasInterceptor=${!!button._hasClickInterceptor}`); + }); + }); + } + }; + + // Add test function for the new send button interception functionality + window.testSendButtonInterception = function () { + console.log("🧪 ===== TESTING SEND BUTTON INTERCEPTION ====="); + + // Add some test text to an input field + const testInput = document.querySelector('textarea') || document.querySelector('input[type="text"]') || document.querySelector('[contenteditable="true"]'); + if (testInput) { + const testText = "My email is john.doe@example.com and my phone is 555-123-4567"; + if (testInput.value !== undefined) { + testInput.value = testText; + } else { + testInput.textContent = testText; + } + testInput.dispatchEvent(new Event('input', { bubbles: true })); + console.log("🧪 Added test sensitive data to input field"); + + // Try to find and click send button + const websiteConfig = getCurrentWebsiteConfig(); + if (websiteConfig) { + websiteConfig.selectors.forEach(selector => { + const button = document.querySelector(selector); + if (button) { + console.log("🧪 Found send button, testing click interception..."); + console.log("🧪 Button has interceptor:", !!button._hasClickInterceptor); + button.click(); + return; + } + }); + console.log("🧪 No send button found for current website"); + } else { + console.log("🧪 Current website not configured for send button interception"); + } + } else { + console.log("🧪 No input field found for testing"); + } + }; + // Initialize content analysis - auto-trigger once per page console.log("🔧 Initializing content analysis..."); @@ -264,11 +1191,22 @@ }); }); - observer.observe(document.body, { childList: true, subtree: true }); + // Ensure document.body exists before observing + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } else { + // Wait for document.body to be available + document.addEventListener('DOMContentLoaded', () => { + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } + }); + } // Listen for navigation events (for SPAs) window.addEventListener("popstate", handleNavigation); + // Check for URL changes periodically (for SPAs) setInterval(handleNavigation, 2000); } diff --git a/Authenticator/repo/manifest.json b/Authenticator/repo/manifest.json index dffcd56ee..d07149489 100644 --- a/Authenticator/repo/manifest.json +++ b/Authenticator/repo/manifest.json @@ -1,8 +1,8 @@ { "manifest_version": 3, - "name": "SAML Authenticator & Sensitive Content Detector", - "version": "1.0.0", - "description": "Handles Okta SAML authentication and detects sensitive content using AI", + "name": "SAML Authenticator & AI Redaction Guard", + "version": "1.1.0", + "description": "Handles Okta SAML authentication and automatically redacts sensitive data on AI websites (ChatGPT, Gemini, Claude, etc.)", "icons": { "16": "Okta.png", "32": "Okta.png", @@ -17,7 +17,15 @@ "host_permissions": [ "https://integrator-2373294.okta.com/*", "https://*.okta.com/*", - "https://generativelanguage.googleapis.com/*" + "https://generativelanguage.googleapis.com/*", + "https://chatgpt.com/*", + "https://*.openai.com/*", + "https://gemini.google.com/*", + "https://bard.google.com/*", + "https://perplexity.ai/*", + "https://claude.ai/*", + "https://*.anthropic.com/*", + "https://poe.com/*" ], "action": { "default_popup": "popup.html", @@ -36,7 +44,19 @@ "resources": [ "auth-success.html", "auth-success.js", - "popup.html" + "popup.html", + "public/libs/xregexp.js", + "public/ptr/apipatterns.js", + "public/ptr/apiPatterns_new.js", + "public/ptr/piiPatterns.js", + "public/ptr/fiPatterns.js", + "public/ptr/fiPatterns_new.js", + "public/ptr/cryptoPatterns.js", + "public/ptr/cryptoPatterns_new.js", + "public/ptr/medPatterns.js", + "public/ptr/medPatterns_new.js", + "public/ptr/networkPatterns.js", + "public/ptr/networkPatterns_new.js" ], "matches": [ "" diff --git a/Authenticator/repo/public/libs/xregexp.js b/Authenticator/repo/public/libs/xregexp.js new file mode 100644 index 000000000..d3b5d4bb4 --- /dev/null +++ b/Authenticator/repo/public/libs/xregexp.js @@ -0,0 +1,24 @@ +// Minimal XRegExp replacement for the extension +function XRegExp(pattern, flags) { + // If pattern is already a string, create a regular expression + if (typeof pattern === 'string') { + return new RegExp(pattern, flags); + } + + // If pattern is already a RegExp, return it as is + if (pattern instanceof RegExp) { + return pattern; + } + + // Fallback + return new RegExp(pattern, flags); +} + +// Make XRegExp available globally +if (typeof window !== 'undefined') { + window.XRegExp = XRegExp; +} else if (typeof global !== 'undefined') { + global.XRegExp = XRegExp; +} else if (typeof self !== 'undefined') { + self.XRegExp = XRegExp; +} \ No newline at end of file diff --git a/Authenticator/repo/public/ptr/apiPatterns_new.js b/Authenticator/repo/public/ptr/apiPatterns_new.js new file mode 100644 index 000000000..e69de29bb diff --git a/Authenticator/repo/public/ptr/apipatterns.js b/Authenticator/repo/public/ptr/apipatterns.js new file mode 100644 index 000000000..f5f265391 --- /dev/null +++ b/Authenticator/repo/public/ptr/apipatterns.js @@ -0,0 +1,1257 @@ +const apiPatterns = { + + + +googleAPIKey: { + type: "Google API Key", + pattern: XRegExp('\\b(?:AIZA|aiza|AiZA|aIZA|Aiza|aiZA|AIza|AiZa)[0-9A-Za-z\\-_]{25,43}\\b'), + description: "Matches Google API keys prefixed with 'AIza'.", + tags: ["Google", "API Key"] +}, + + +adobeSignAPIKey: { + type: "Adobe Sign API Key", + pattern: XRegExp('^SIGN-[A-Za-z0-9]{20,25}$'), + description: "Matches Adobe Sign API keys starting with 'SIGN-'.", + tags: ["Adobe", "API Key"] +}, + + +adobeAnalyticsAPIKey: { + type: "Adobe Analytics API Key", + pattern: XRegExp('^AKEY-[A-Za-z0-9]{20,25}$'), + description: "Matches Adobe Analytics API keys starting with 'AKEY-'.", + tags: ["Adobe", "API Key"] +}, + + +adobeClientID: { + type: "Adobe API Key (Client ID)", + pattern: XRegExp('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'), + description: "Matches Adobe Client ID in UUID format.", + tags: ["Adobe", "API Key"] +}, + + +ablyAPIKey: { + type: "Ably API Key", + pattern: XRegExp('^ably_[a-zA-Z0-9]{32}$'), + description: "Matches Ably API keys prefixed with 'ably_'.", + tags: ["Ably", "API Key"] +}, + + +accuWeatherAPIKey: { + type: "AccuWeather API Key", + pattern: XRegExp('^[0-9A-Za-z]{32}$'), + description: "Matches 32-character AccuWeather API keys.", + tags: ["AccuWeather", "API Key"] +}, + + +airtableAPIKey: { + type: "Airtable API Key", + pattern: XRegExp('^key[A-Za-z0-9]{14}$'), + description: "Matches Airtable API keys prefixed with 'key'.", + tags: ["Airtable", "API Key"] +}, + + +algoliaAPIKey: { + type: "Algolia API Key", + pattern: XRegExp('^[a-f0-9]{32}$'), + description: "Matches Algolia API keys in hexadecimal format.", + tags: ["Algolia", "API Key"] +}, + + +agoraAPIKey: { + type: "Agora API Key", + pattern: XRegExp('^[A-Za-z0-9]{32}$'), + description: "Matches 32-character Agora API keys.", + tags: ["Agora", "API Key"] +}, + + +awsAccessID: { + type: "AWS Access ID Key", + pattern: XRegExp('\\bAKIA[0-9A-Z]{16}\\b'), + description: "Matches AWS Access ID Keys starting with 'AKIA'.", + tags: ["AWS", "Access Key"] +}, + + +awsSecretKey: { + type: "AWS Secret Key", + pattern: XRegExp('\\b[0-9a-zA-Z/+]{40}\\b'), + description: "Matches 40-character AWS Secret Keys.", + tags: ["AWS", "Secret Key"] +}, + + +amazonAuthToken: { + type: "Amazon Auth Token", + pattern: XRegExp('^amzn\\.mws\\.[0-9a-f]{8}-[0-9a-f]{4}-10-[0-9a-f]{4}-[0-9a-f]{12}$'), + description: "Matches Amazon Auth Tokens in a specific pattern.", + tags: ["Amazon", "Auth Token"] +}, + + +amplitudeAPIKey: { + type: "Amplitude API Key", + pattern: XRegExp('^amp_[A-Za-z0-9]{20,32}$'), + description: "Matches Amplitude API keys prefixed with 'amp_'.", + tags: ["Amplitude", "API Key"] +}, + + +applePrivateKey: { + type: "Apple Private Key", + pattern: XRegExp('^-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=\\s]+-----END PRIVATE KEY-----$'), + description: "Matches Apple Private Keys in PEM format.", + tags: ["Apple", "Private Key"] +}, + + +atlassianAPIKey: { + type: "Atlassian API Key", + pattern: XRegExp('^atl_[a-zA-Z0-9]{24,40}$'), + description: "Matches Atlassian API keys prefixed with 'atl_'.", + tags: ["Atlassian", "API Key"] +}, + + +autodeskAPIKey: { + type: "Autodesk API Key", + pattern: XRegExp('^ads[A-Za-z0-9]{20,32}$'), + description: "Matches Autodesk API keys prefixed with 'ads'.", + tags: ["Autodesk", "API Key"] +}, + + +autopilotAPIKey: { + type: "Autopilot API Key", + pattern: XRegExp('^AP-[A-Za-z0-9]{30}$'), + description: "Matches Autopilot API keys prefixed with 'AP-'.", + tags: ["Autopilot", "API Key"] +}, + + +basecampAPIKey: { + type: "Basecamp API Key", + pattern: XRegExp('^BC[A-Za-z0-9]{24}$'), + description: "Matches Basecamp API keys prefixed with 'BC'.", + tags: ["Basecamp", "API Key"] +}, + + +benchlingAPIKey: { + type: "Benchling API Key", + pattern: XRegExp('^bench_[A-Za-z0-9]{32}$'), + description: "Matches Benchling API keys prefixed with 'bench_'.", + tags: ["Benchling", "API Key"] +}, + + +bitbucketAPIKey: { + type: "Bitbucket API Key", + pattern: XRegExp('^bitbucket_[A-Za-z0-9]{20,30}$'), + description: "Matches Bitbucket API keys prefixed with 'bitbucket_'.", + tags: ["Bitbucket", "API Key"] +}, + + +boxDeveloperToken: { + type: "Box Developer Token", + pattern: XRegExp('^[A-Za-z0-9_-]{64}$'), + description: "Matches 64-character Box Developer Tokens.", + tags: ["Box", "Developer Token"] +}, + + +calendlyAPIKey: { + type: "Calendly API Key", + pattern: XRegExp('^api_key-[a-zA-Z0-9]{40}$'), + description: "Matches Calendly API keys prefixed with 'api_key-'.", + tags: ["Calendly", "API Key"] +}, + + +ciscoWebexAPIKey: { + type: "Cisco Webex API Key", + pattern: XRegExp('^MC[A-Za-z0-9_-]{20,40}$'), + description: "Matches Cisco Webex API keys prefixed with 'MC'.", + tags: ["Cisco", "Webex", "API Key"] +}, + + +clarityAIKey: { + type: "Clarity AI API Key", + pattern: XRegExp('^[a-f0-9]{36}$'), + description: "Matches 36-character Clarity AI API keys.", + tags: ["Clarity AI", "API Key"] +}, + + +clickupAPIKey: { + type: "ClickUp API Key", + pattern: XRegExp('^cl_[A-Za-z0-9]{32}$'), + description: "Matches ClickUp API keys prefixed with 'cl_'.", + tags: ["ClickUp", "API Key"] +}, + + +cloudflareAPIToken: { + type: "Cloudflare API Token", + pattern: XRegExp('^[A-Fa-f0-9]{32}$'), + description: "Matches 32-character Cloudflare API tokens.", + tags: ["Cloudflare", "API Token"] +}, + + +cloudinaryAPIKey: { + type: "Cloudinary API Key", + pattern: XRegExp('^CLOUDK-[A-Za-z0-9_-]{20}$'), + description: "Matches Cloudinary API keys prefixed with 'CLOUDK-'.", + tags: ["Cloudinary", "API Key"] +}, + + +cockroachDBAPIKey: { + type: "CockroachDB API Key", + pattern: XRegExp('^cockroach_[A-Za-z0-9]{24,40}$'), + description: "Matches CockroachDB API keys prefixed with 'cockroach_'.", + tags: ["CockroachDB", "API Key"] +}, + + +codaAPIToken: { + type: "Coda API Token", + pattern: XRegExp('^[A-Za-z0-9]{40}$'), + description: "Matches 40-character Coda API tokens.", + tags: ["Coda", "API Token"] +}, + + +coinbaseAPIKey: { + type: "Coinbase API Key", + pattern: XRegExp('^[a-zA-Z0-9]{32}$'), + description: "Matches 32-character Coinbase API keys.", + tags: ["Coinbase", "API Key"] +}, + + +contentfulDeliveryAPIKey: { + type: "Contentful Delivery API Key", + pattern: XRegExp('^[a-zA-Z0-9_-]{43}$'), + description: "Matches 43-character Contentful API keys.", + tags: ["Contentful", "Delivery API Key"] +}, + + +courierAPIKey: { + type: "Courier API Key", + pattern: XRegExp('^courier_[a-zA-Z0-9]{50}$'), + description: "Matches Courier API keys prefixed with 'courier_'.", + tags: ["Courier", "API Key"] +}, + + +dashlaneAPIKey: { + type: "Dashlane API Key", + pattern: XRegExp('^dashlane_[A-Za-z0-9]{24,}$'), + description: "Matches Dashlane API keys prefixed with 'dashlane_'.", + tags: ["Dashlane", "API Key"] +}, + + +datadogAPIKey: { + type: "Datadog API Key", + pattern: XRegExp('^DDOG[a-fA-F0-9]{28}$'), + description: "Matches Datadog API keys prefixed with 'DDOG'.", + tags: ["Datadog", "API Key"] +}, + + +dailymotionAPIKey: { + type: "Dailymotion API Key", + pattern: XRegExp('^[A-Za-z0-9]{40}$'), + description: "Matches 40-character Dailymotion API keys.", + tags: ["Dailymotion", "API Key"] +}, + + +deezerAPIKey: { + type: "Deezer API Key", + pattern: XRegExp('^[A-Za-z0-9]{32}$'), + description: "Matches 32-character Deezer API keys.", + tags: ["Deezer", "API Key"] +}, + + +dockerAPIToken: { + type: "Docker API Token", + pattern: XRegExp('^docker_[A-Za-z0-9]{32}$'), + description: "Matches Docker API tokens prefixed with 'docker_'.", + tags: ["Docker", "API Token"] +}, + + +dockerHubAPIToken: { + type: "Docker Hub API Token", + pattern: XRegExp('^[0-9A-Za-z_-]{32}$'), + description: "Matches 32-character Docker Hub API tokens.", + tags: ["Docker Hub", "API Token"] +}, + + +docusignAPIKey: { + type: "Docusign API Key", + pattern: XRegExp('^DS[A-Za-z0-9]{20}$'), + description: "Matches Docusign API keys prefixed with 'DS'.", + tags: ["Docusign", "API Key"] +}, + + +driftAPIToken: { + type: "Drift API Token", + pattern: XRegExp('^drift_[A-Za-z0-9]{40}$'), + description: "Matches Drift API tokens prefixed with 'drift_'.", + tags: ["Drift", "API Token"] +}, + + +dropboxAPIKey: { + type: "Dropbox API Key", + pattern: XRegExp('^[a-z0-9]{40,50}$'), + description: "Matches Dropbox API keys, typically 40-50 characters.", + tags: ["Dropbox", "API Key"] +}, + + +duckduckgoAPIKey: { + type: "DuckDuckGo API Key", + pattern: XRegExp('^duck[A-Za-z0-9]{20}$'), + description: "Matches DuckDuckGo API keys prefixed with 'duck'.", + tags: ["DuckDuckGo", "API Key"] +}, + + +ebayAPIKey: { + type: "eBay API Key", + pattern: XRegExp('^[0-9a-zA-Z]{24}$'), + description: "Matches 24-character eBay API keys.", + tags: ["eBay", "API Key"] +}, + + +elasticCloudAPIKey: { + type: "Elastic Cloud API Key", + pattern: XRegExp('^[a-zA-Z0-9]{32}$'), + description: "Matches Elastic Cloud API keys, typically 32 characters.", + tags: ["Elastic Cloud", "API Key"] +}, + + +envoyAPIKey: { + type: "Envoy API Key", + pattern: XRegExp('^env_[A-Za-z0-9]{40}$'), + description: "Matches Envoy API keys prefixed with 'env_'.", + tags: ["Envoy", "API Key"] +}, + + +etsyAPIKey: { + type: "Etsy API Key", + pattern: XRegExp('^key_[A-Za-z0-9]{32}$'), + description: "Matches Etsy API keys prefixed with 'key_'.", + tags: ["Etsy", "API Key"] +}, + + +eventbriteAPIKey: { + type: "Eventbrite API Key", + pattern: XRegExp('^[A-Za-z0-9]{32}$'), + description: "Matches Eventbrite API keys, typically 32 characters.", + tags: ["Eventbrite", "API Key"] +}, + + +expensifyAPIKey: { + type: "Expensify API Key", + pattern: XRegExp('^exp_[A-Za-z0-9]{40}$'), + description: "Matches Expensify API keys prefixed with 'exp_'.", + tags: ["Expensify", "API Key"] +}, + + +facebookGraphAPIToken: { + type: "Facebook Graph API Token", + pattern: XRegExp('^EAAG[a-zA-Z0-9]{30,60}$'), + description: "Matches Facebook Graph API tokens prefixed with 'EAAG'.", + tags: ["Facebook", "Graph API", "Token"] +}, + + +facebookAccessToken: { + type: "Facebook Access Token", + pattern: XRegExp('^EAACEdEose0cBA[0-9A-Za-z]+$'), + description: "Matches Facebook Access Tokens with common prefix.", + tags: ["Facebook", "Access Token"] +}, + + +figmaAPIToken: { + type: "Figma API Token", + pattern: XRegExp('^figd_[a-f0-9]{32,64}$'), + description: "Matches Figma API tokens prefixed with 'figd_'.", + tags: ["Figma", "API Token"] +}, + + +firebaseWebAPIKey: { + type: "Firebase Web API Key", + pattern: XRegExp('^AAAA[A-Za-z0-9_-]{20,50}$'), + description: "Matches Firebase Web API keys prefixed with 'AAAA'.", + tags: ["Firebase", "Web API Key"] +}, + + +flexportAPIKey: { + type: "Flexport API Key", + pattern: XRegExp('(?