From 13d9821b8d82b4fb47f58173667b84d9b3eb93a4 Mon Sep 17 00:00:00 2001 From: becarta Date: Sat, 10 May 2025 03:31:18 +0200 Subject: [PATCH] feat: Enhance Enterprise App Protection extension with monitoring and UI improvements - Introduce error reporting and performance monitoring in background.js to track API calls and processing times. - Implement health check system to ensure the extension's operational status and log issues. - Add caching and encryption utilities in content.js for improved link analysis and data validation. - Refactor link analysis to process in batches, enhancing performance and user experience. - Update UI in domains_management.html and options.html for better usability and aesthetics, including responsive design and improved layout. - Enhance popup.html to display suspicious links with better styling and functionality. - Modify manifest.json to include new permissions and host access for Safe Browsing API. --- background.js | 229 +++++++++++++++++++- content.js | 389 ++++++++++++++++++++++++---------- domains_management.html | 458 +++++++++++++++++++++++++++------------- manifest.json | 26 ++- options.html | 276 +++++++++++++++++------- popup.html | 238 ++++++++++++++++----- 6 files changed, 1220 insertions(+), 396 deletions(-) diff --git a/background.js b/background.js index 7ae8429..6a107ff 100644 --- a/background.js +++ b/background.js @@ -3,6 +3,136 @@ console.log("Background service worker loaded"); let domainsDB = {}; +let SAFE_BROWSING_API_KEY = ""; +const API_CALL_LIMIT = 100; // Maximum number of API calls per minute +let apiCallCount = 0; +let lastResetTime = Date.now(); + +// Error reporting service +const errorReporter = { + errors: [], + maxErrors: 100, + + log(error, context = {}) { + const errorEntry = { + timestamp: Date.now(), + error: error.message || error, + stack: error.stack, + context + }; + + this.errors.push(errorEntry); + if (this.errors.length > this.maxErrors) { + this.errors.shift(); + } + + // Send to monitoring service if configured + if (this.monitoringEndpoint) { + this.sendToMonitoring(errorEntry); + } + + console.error('Error logged:', errorEntry); + }, + + async sendToMonitoring(errorEntry) { + try { + await fetch(this.monitoringEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(errorEntry) + }); + } catch (e) { + console.error('Failed to send error to monitoring:', e); + } + }, + + getErrors() { + return [...this.errors]; + }, + + clearErrors() { + this.errors = []; + } +}; + +// Performance monitoring +const performanceMonitor = { + metrics: { + apiCalls: 0, + processingTime: 0, + errors: 0 + }, + + startTimer() { + return performance.now(); + }, + + endTimer(startTime) { + return performance.now() - startTime; + }, + + logMetric(name, value) { + this.metrics[name] = (this.metrics[name] || 0) + value; + }, + + getMetrics() { + return { ...this.metrics }; + }, + + reset() { + this.metrics = { + apiCalls: 0, + processingTime: 0, + errors: 0 + }; + } +}; + +// Health check system +const healthCheck = { + lastCheck: Date.now(), + status: 'healthy', + issues: [], + + async check() { + const startTime = performanceMonitor.startTimer(); + try { + // Check storage access + await chrome.storage.local.get(['domainsDB']); + + // Check API key + if (!SAFE_BROWSING_API_KEY) { + throw new Error('API key not configured'); + } + + // Check domains database + if (!domainsDB || Object.keys(domainsDB).length === 0) { + throw new Error('Domains database is empty'); + } + + this.status = 'healthy'; + this.issues = []; + } catch (error) { + this.status = 'unhealthy'; + this.issues.push({ + timestamp: Date.now(), + error: error.message + }); + errorReporter.log(error, { component: 'healthCheck' }); + } + + performanceMonitor.logMetric('processingTime', performanceMonitor.endTimer(startTime)); + this.lastCheck = Date.now(); + }, + + getStatus() { + return { + status: this.status, + lastCheck: this.lastCheck, + issues: [...this.issues] + }; + } +}; async function updateDomainsDB() { chrome.storage.local.get(["domainsDBURL"], async function (result) { @@ -85,6 +215,29 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); return true; // Keep message channel open for async response } + + if (message.action === "checkSafeBrowsing") { + checkSafeBrowsing(message.url) + .then(result => sendResponse(result)) + .catch(error => { + console.error("Safe Browsing API error:", error); + sendResponse({ isUnsafe: false }); + }); + return true; // Required for async response + } + + if (message.action === "getHealthStatus") { + sendResponse(healthCheck.getStatus()); + return true; + } + + if (message.action === "getMetrics") { + sendResponse({ + performance: performanceMonitor.getMetrics(), + errors: errorReporter.getErrors() + }); + return true; + } }); // Debug Mode: Keep-alive mechanism for testing @@ -97,4 +250,78 @@ if (DEBUG_MODE) { console.log("keepAlive alarm triggered, service worker is active"); } }); -} \ No newline at end of file +} + +// Reset API call count every minute +setInterval(() => { + apiCallCount = 0; + lastResetTime = Date.now(); +}, 60000); + +// Check URL against Google Safe Browsing API +async function checkSafeBrowsing(url) { + const startTime = performanceMonitor.startTimer(); + + try { + // Check rate limit + if (apiCallCount >= API_CALL_LIMIT) { + throw new Error('API call limit reached'); + } + + // Get API key from storage + if (!SAFE_BROWSING_API_KEY) { + const result = await chrome.storage.local.get(["safeBrowsingApiKey"]); + SAFE_BROWSING_API_KEY = result.safeBrowsingApiKey; + if (!SAFE_BROWSING_API_KEY) { + throw new Error('Safe Browsing API key not set'); + } + } + + apiCallCount++; + performanceMonitor.logMetric('apiCalls', 1); + + const response = await fetch( + `https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${SAFE_BROWSING_API_KEY}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + client: { + clientId: "enterprise-app-protection", + clientVersion: "1.0" + }, + threatInfo: { + threatTypes: ["MALWARE", "SOCIAL_ENGINEERING"], + platformTypes: ["ANY_PLATFORM"], + threatEntryTypes: ["URL"], + threatEntries: [{ url }] + } + }) + } + ); + + if (!response.ok) { + throw new Error(`API responded with status: ${response.status}`); + } + + const data = await response.json(); + performanceMonitor.logMetric('processingTime', performanceMonitor.endTimer(startTime)); + return { isUnsafe: data.matches ? true : false }; + } catch (error) { + performanceMonitor.logMetric('errors', 1); + errorReporter.log(error, { url, apiCallCount }); + return { isUnsafe: false }; + } +} + +// Listen for storage changes to update API key +chrome.storage.onChanged.addListener((changes, namespace) => { + if (namespace === "local" && changes.safeBrowsingApiKey) { + SAFE_BROWSING_API_KEY = changes.safeBrowsingApiKey.newValue; + } +}); + +// Add periodic health checks +setInterval(() => { + healthCheck.check(); +}, 300000); // Every 5 minutes \ No newline at end of file diff --git a/content.js b/content.js index ed5d1cf..71c914f 100644 --- a/content.js +++ b/content.js @@ -1,12 +1,117 @@ // content.js -let SAFE_BROWSING_API_KEY = ""; +// Encryption utilities +const encryption = { + async encrypt(data) { + try { + const encoder = new TextEncoder(); + const dataBuffer = encoder.encode(JSON.stringify(data)); + const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } catch (error) { + console.error('Encryption error:', error); + return null; + } + }, + + async validateData(data) { + if (!data) return false; + try { + const hash = await this.encrypt(data); + return !!hash; + } catch (error) { + console.error('Validation error:', error); + return false; + } + } +}; + +// Cache management +const cacheManager = { + cache: new Map(), + maxSize: 1000, + maxAge: 3600000, // 1 hour + + set(key, value) { + if (this.cache.size >= this.maxSize) { + const oldestKey = this.cache.keys().next().value; + this.cache.delete(oldestKey); + } + this.cache.set(key, { + value, + timestamp: Date.now() + }); + }, + + get(key) { + const item = this.cache.get(key); + if (!item) return null; + if (Date.now() - item.timestamp > this.maxAge) { + this.cache.delete(key); + return null; + } + return item.value; + }, + + clear() { + this.cache.clear(); + } +}; + +// Performance monitoring +const performanceMonitor = { + metrics: { + linkChecks: 0, + apiCalls: 0, + processingTime: 0 + }, + + startTimer() { + return performance.now(); + }, + + endTimer(startTime) { + return performance.now() - startTime; + }, + + logMetric(name, value) { + this.metrics[name] = (this.metrics[name] || 0) + value; + }, + + getMetrics() { + return { ...this.metrics }; + }, + + reset() { + this.metrics = { + linkChecks: 0, + apiCalls: 0, + processingTime: 0 + }; + } +}; + let domainsDB = {}; let trustedDomains = []; let blockedDomains = []; let warningTemplate = "Warning: This link claims to be {app} but goes to an unofficial domain."; let isDomainsLoaded = false; +// Add rate limiting for API calls +const rateLimiter = { + lastCall: 0, + minInterval: 1000, // 1 second between calls + canMakeCall() { + const now = Date.now(); + if (now - this.lastCall >= this.minInterval) { + this.lastCall = now; + return true; + } + return false; + } +}; + // ✅ Get the top-level domain (TLD) function getTopLevelDomain(hostname) { const domainParts = hostname.split("."); @@ -37,143 +142,191 @@ function isValidDomain(domain, validDomains, trustedDomains) { // ✅ Check Google Safe Browsing API for dangerous links async function checkGoogleSafeBrowsing(url) { - console.log("🔍 Checking Google Safe Browsing for:", url); + if (!rateLimiter.canMakeCall()) { + console.warn("⚠️ Rate limit exceeded for Safe Browsing API calls"); + return false; + } - if (!SAFE_BROWSING_API_KEY) { - console.warn("⚠️ API key is not set. Skipping Safe Browsing check."); - return false; - } + try { + const response = await chrome.runtime.sendMessage({ + action: "checkSafeBrowsing", + url: url + }); + return response.isUnsafe || false; + } catch (error) { + console.error("Error checking Safe Browsing:", error); + return false; + } +} - const response = await fetch(`https://safebrowsing.googleapis.com/v4/threatMatches:find?key=${SAFE_BROWSING_API_KEY}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - client: { clientId: "enterprise-app-protection", clientVersion: "1.0" }, - threatInfo: { - threatTypes: ["MALWARE", "SOCIAL_ENGINEERING"], - platformTypes: ["ANY_PLATFORM"], - threatEntryTypes: ["URL"], - threatEntries: [{ url }] - } - }) - }); - - const data = await response.json(); - console.log("🔹 Google Safe Browsing API Response:", data); - return data.matches ? true : false; +// Sanitize warning template to prevent XSS +function sanitizeWarningTemplate(template, appName) { + const div = document.createElement('div'); + div.textContent = template.replace("{app}", appName); + return div.textContent; } // ✅ Scan page content for suspicious links -function analyzePageContent() { - if (!isDomainsLoaded || !domainsDB || Object.keys(domainsDB).length === 0) { - console.warn("⚠️ domainsDB is not ready yet. Skipping analysis."); - return; - } +async function analyzePageContent() { + const startTime = performanceMonitor.startTimer(); + + if (!isDomainsLoaded || !domainsDB || Object.keys(domainsDB).length === 0) { + console.warn("⚠️ domainsDB is not ready yet. Skipping analysis."); + return; + } - const links = document.querySelectorAll("a:not(.checked-by-extension)"); - let newFlaggedLinks = new Set(); + const links = document.querySelectorAll("a:not(.checked-by-extension)"); + let newFlaggedLinks = new Set(); + let processedLinks = 0; - links.forEach(link => { - try { - // ✅ Validate the URL before using it - let url; - try { - url = new URL(link.href); - } catch (e) { - console.warn("⚠️ Skipping invalid URL:", link.href); - return; // Stop processing this link - } + // Process links in batches of 50 + const batchSize = 50; + const batches = Array.from(links).reduce((acc, link, i) => { + const batchIndex = Math.floor(i / batchSize); + if (!acc[batchIndex]) acc[batchIndex] = []; + acc[batchIndex].push(link); + return acc; + }, []); - const domain = url.hostname.toLowerCase(); - const linkText = link.innerText.trim(); + for (const batch of batches) { + await Promise.all(batch.map(async link => { + try { + // Check cache first + const cachedResult = cacheManager.get(link.href); + if (cachedResult) { + if (cachedResult.isUnsafe) { + newFlaggedLinks.add(link.href); + addWarningToLink(link, cachedResult.appName); + } + return; + } - // ✅ Mark link as processed - link.classList.add("checked-by-extension"); + // Validate the URL + let url; + try { + url = new URL(link.href); + } catch (e) { + console.warn("⚠️ Skipping invalid URL:", link.href); + return; + } - // ✅ Skip trusted domains - if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) { - console.log("Skipping trusted domain:", domain); - return; - } + const domain = url.hostname.toLowerCase(); + const linkText = link.innerText.trim(); + processedLinks++; - let matchedApp = null; + // Skip trusted domains + if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) { + return; + } - // ✅ Find the most specific matching app name (ONLY full word matches) - for (const [appName, validDomains] of Object.entries(domainsDB)) { - const regex = new RegExp(`\\b${appName}\\b`, "i"); // Ensure full-word match - if (regex.test(linkText)) { - matchedApp = appName; // Keep the most specific match - } - } + let matchedApp = null; + for (const [appName, validDomains] of Object.entries(domainsDB)) { + const regex = new RegExp(`\\b${appName}\\b`, "i"); + if (regex.test(linkText)) { + matchedApp = appName; + } + } - if (matchedApp && domainsDB[matchedApp]) { - const validDomains = domainsDB[matchedApp]; - const isValid = isValidDomain(domain, validDomains, trustedDomains); + if (matchedApp && domainsDB[matchedApp]) { + const validDomains = domainsDB[matchedApp]; + const isValid = isValidDomain(domain, validDomains, trustedDomains); - if (!isValid) { - newFlaggedLinks.add(url.href); + if (!isValid) { + newFlaggedLinks.add(url.href); + const isUnsafe = await checkGoogleSafeBrowsing(url.href); + + // Cache the result + cacheManager.set(link.href, { + isUnsafe, + appName: matchedApp + }); - // ✅ Add warning immediately - const warning = document.createElement("div"); - warning.classList.add("warning-alert"); - warning.style.cssText = ` - background: #fff3cd; - color: #856404; - padding: 10px; - border: 1px solid #ffeeba; - border-radius: 4px; - margin: 5px 0; - font-size: 14px; - `; + addWarningToLink(link, matchedApp, isUnsafe); + } + } + } catch (e) { + console.error("Unexpected error processing link:", e); + } + })); - warning.textContent = `⚠️ ${warningTemplate.replace("{app}", matchedApp)}`; - warning.setAttribute("title", `This link goes to ${domain} instead of an official ${matchedApp} domain.`); - link.parentElement.insertBefore(warning, link.nextSibling); + // Add a small delay between batches to prevent UI blocking + await new Promise(resolve => setTimeout(resolve, 50)); + } - // ✅ Update warning if Google Safe Browsing confirms danger - checkGoogleSafeBrowsing(url.href).then(isUnsafe => { - if (isUnsafe) { - warning.textContent = `⚠️ This link is confirmed dangerous by Google Safe Browsing!`; - } - }); - } - } - } catch (e) { - console.error("Unexpected error processing link:", e); - } - }); + // Update metrics + performanceMonitor.logMetric('linkChecks', processedLinks); + performanceMonitor.logMetric('processingTime', performanceMonitor.endTimer(startTime)); - // ✅ Store flagged links without waiting for Safe Browsing API - chrome.storage.local.get(["flaggedLinks", "totalSuspiciousLinks"], function (result) { - let existingLinks = new Set(result.flaggedLinks || []); - let totalCount = existingLinks.size; + // Store flagged links with encryption + if (newFlaggedLinks.size > 0) { + const storageData = { + flaggedLinks: Array.from(newFlaggedLinks), + timestamp: Date.now() + }; - let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link)); + if (await encryption.validateData(storageData)) { + chrome.storage.local.get(["flaggedLinks", "totalSuspiciousLinks"], async function (result) { + let existingLinks = new Set(result.flaggedLinks || []); + let totalCount = existingLinks.size; - if (newLinksToAdd.length > 0) { - totalCount += newLinksToAdd.length; + let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link)); - chrome.storage.local.set({ - totalSuspiciousLinks: totalCount, - flaggedLinks: [...existingLinks, ...newLinksToAdd] - }, () => { - chrome.runtime.sendMessage({ action: "updatePopup", flaggedLinks: [...existingLinks, ...newLinksToAdd], totalCount }); - }); - } - }); + if (newLinksToAdd.length > 0) { + totalCount += newLinksToAdd.length; + + const updatedData = { + totalSuspiciousLinks: totalCount, + flaggedLinks: [...existingLinks, ...newLinksToAdd], + lastUpdate: Date.now() + }; + + if (await encryption.validateData(updatedData)) { + chrome.storage.local.set(updatedData, () => { + chrome.runtime.sendMessage({ + action: "updatePopup", + flaggedLinks: [...existingLinks, ...newLinksToAdd], + totalCount, + metrics: performanceMonitor.getMetrics() + }); + }); + } + } + }); + } + } +} + +// Helper function to add warning to link +function addWarningToLink(link, appName, isUnsafe = false) { + const warning = document.createElement("div"); + warning.classList.add("warning-alert"); + warning.style.cssText = ` + background: #fff3cd; + color: #856404; + padding: 10px; + border: 1px solid #ffeeba; + border-radius: 4px; + margin: 5px 0; + font-size: 14px; + `; + + warning.textContent = isUnsafe + ? `⚠️ This link is confirmed dangerous by Google Safe Browsing!` + : `⚠️ ${sanitizeWarningTemplate(warningTemplate, appName)}`; + + warning.setAttribute("title", `This link goes to ${link.hostname} instead of an official ${appName} domain.`); + link.parentElement.insertBefore(warning, link.nextSibling); } // ✅ Load domains and start analysis once ready function loadDomainsAndAnalyze() { - chrome.storage.local.get(["domainsDB", "trustedDomains", "blockedDomains", "safeBrowsingApiKey", "warningTemplate"], function (result) { + chrome.storage.local.get(["domainsDB", "trustedDomains", "blockedDomains", "warningTemplate"], function (result) { domainsDB = result.domainsDB || {}; trustedDomains = result.trustedDomains || []; blockedDomains = result.blockedDomains || []; - SAFE_BROWSING_API_KEY = result.safeBrowsingApiKey || ""; warningTemplate = result.warningTemplate || "Warning: This link claims to be {app} but goes to an unofficial domain."; - console.log("✅ Loaded API Key:", SAFE_BROWSING_API_KEY); - isDomainsLoaded = true; console.log("✅ Domains database loaded:", domainsDB); + isDomainsLoaded = true; analyzePageContent(); }); } @@ -213,6 +366,7 @@ const enhancedAnalyzePageContent = withErrorHandling(debouncedAnalyzePageContent // Update the observer to use the enhanced version function observePageForLinks() { const observer = new MutationObserver(() => enhancedAnalyzePageContent()); + window.pageObserver = observer; // Store for cleanup observer.observe(document.body, { childList: true, subtree: true }); @@ -222,7 +376,6 @@ function observePageForLinks() { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { observer.observe(iframeDoc.body, { childList: true, subtree: true }); - console.log("Observing links inside iframe:", iframe.src); } } catch (e) { console.warn("Cannot access iframe due to cross-origin restrictions:", iframe.src); @@ -230,11 +383,23 @@ function observePageForLinks() { }); } - // ✅ Run iframe observer every 3 seconds for Office 365 dynamic content - setInterval(observeIframes, 3000); + window.iframeInterval = setInterval(observeIframes, 3000); } // ✅ Initialize extension scanning document.addEventListener("DOMContentLoaded", loadDomainsAndAnalyze); window.addEventListener("load", loadDomainsAndAnalyze); -observePageForLinks(); \ No newline at end of file +observePageForLinks(); + +// Add cleanup for observers +function cleanup() { + if (window.pageObserver) { + window.pageObserver.disconnect(); + } + if (window.iframeInterval) { + clearInterval(window.iframeInterval); + } +} + +// Add cleanup on page unload +window.addEventListener("unload", cleanup); \ No newline at end of file diff --git a/domains_management.html b/domains_management.html index 1ed49bc..bd1a14d 100644 --- a/domains_management.html +++ b/domains_management.html @@ -5,197 +5,361 @@ Enterprise App Protection - Domain Management +
-

Enterprise App Protection - Domain Management

- - -

✅ How to Use

- -

-    example.com
-    secure-site.org
-    sub.example.com
-    malicious-site.net
-        
- -

✅ Trusted Domains (Safe List)

- - -

❌ Blocked Domains (Unsafe List)

- - -
- +
+

Domain Management

+

Manage trusted and blocked domains for enterprise application protection

+ +
+
+

🔒 Trusted Domains

+ +
+ +
+ +
+
+ +
+
+

🚫 Blocked Domains

+ +
+ +
+ +
+
+ +
- -