diff --git a/Authenticator/Authenticator.crx b/Authenticator/Authenticator.crx index a0b6a91a6..12319eb80 100644 Binary files a/Authenticator/Authenticator.crx and b/Authenticator/Authenticator.crx differ diff --git a/Authenticator/repo/background.js b/Authenticator/repo/background.js index d3673321c..91c23d4d0 100644 --- a/Authenticator/repo/background.js +++ b/Authenticator/repo/background.js @@ -1,5 +1,380 @@ console.log("Background script starting..."); +// ====================== +// SENSITIVE TEXT REDACTION IMPORTS AND CONFIGURATION +// ====================== + +// Import XRegExp and pattern files for text redaction +try { + importScripts( + 'public/libs/xregexp.js', + 'public/ptr/apipatterns.js', + 'public/ptr/piiPatterns.js', + 'public/ptr/fiPatterns.js', + 'public/ptr/cryptoPatterns.js', + 'public/ptr/medPatterns.js', + 'public/ptr/networkPatterns.js' + ); + console.log("โœ… Successfully imported pattern files for text redaction"); +} catch (error) { + console.error("โŒ Failed to import pattern files:", error); +} + +// Combine all patterns for text redaction +const allRedactionPatterns = {}; +try { + if (typeof apiPatterns !== 'undefined') { + allRedactionPatterns.api = apiPatterns; + } + if (typeof personalIdentificationPatterns !== 'undefined') { + allRedactionPatterns.pii = personalIdentificationPatterns; + } + if (typeof fiPatterns !== 'undefined') { + allRedactionPatterns.financial = fiPatterns; + } + if (typeof cryptoPatterns !== 'undefined') { + allRedactionPatterns.crypto = cryptoPatterns; + } + if (typeof medPatterns !== 'undefined') { + allRedactionPatterns.medical = medPatterns; + } + if (typeof networkPatterns !== 'undefined') { + allRedactionPatterns.network = networkPatterns; + } + console.log("โœ… Combined patterns for categories:", Object.keys(allRedactionPatterns)); +} catch (error) { + console.error("โŒ Error combining patterns:", error); +} + +// Default enabled categories for text redaction +const defaultRedactionCategories = { + api: true, + pii: true, + financial: true, + crypto: true, + medical: false, // Disabled by default for performance + network: false // Disabled by default for performance +}; + +// Text redaction configuration +const REDACTION_CONFIG = { + REDACTION_CHAR: "*", + MIN_REVEAL_LENGTH: 2, + MAX_REVEAL_LENGTH: 4, + EMAIL_REDACTION_FORMAT: "prefix***@domain.com" // Special format for emails +}; + +// ====================== +// SENSITIVE TEXT REDACTION FUNCTIONS +// ====================== + +// Initialize redaction settings on install +function initializeRedactionSettings() { + chrome.storage.sync.set({ + enabledRedactionCategories: defaultRedactionCategories, + redactionEnabled: true + }); +} + +// Get enabled redaction categories from storage +function getEnabledRedactionCategories(callback) { + chrome.storage.sync.get(['enabledRedactionCategories'], (result) => { + 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('(?