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.
This commit is contained in:
becarta
2025-05-10 03:31:18 +02:00
parent ba83c95914
commit 13d9821b8d
6 changed files with 1220 additions and 396 deletions

View File

@@ -3,6 +3,136 @@
console.log("Background service worker loaded"); console.log("Background service worker loaded");
let domainsDB = {}; 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() { async function updateDomainsDB() {
chrome.storage.local.get(["domainsDBURL"], async function (result) { 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 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 // Debug Mode: Keep-alive mechanism for testing
@@ -97,4 +250,78 @@ if (DEBUG_MODE) {
console.log("keepAlive alarm triggered, service worker is active"); console.log("keepAlive alarm triggered, service worker is active");
} }
}); });
} }
// 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

View File

@@ -1,12 +1,117 @@
// content.js // 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 domainsDB = {};
let trustedDomains = []; let trustedDomains = [];
let blockedDomains = []; let blockedDomains = [];
let warningTemplate = "Warning: This link claims to be {app} but goes to an unofficial domain."; let warningTemplate = "Warning: This link claims to be {app} but goes to an unofficial domain.";
let isDomainsLoaded = false; 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) // ✅ Get the top-level domain (TLD)
function getTopLevelDomain(hostname) { function getTopLevelDomain(hostname) {
const domainParts = hostname.split("."); const domainParts = hostname.split(".");
@@ -37,143 +142,191 @@ function isValidDomain(domain, validDomains, trustedDomains) {
// ✅ Check Google Safe Browsing API for dangerous links // ✅ Check Google Safe Browsing API for dangerous links
async function checkGoogleSafeBrowsing(url) { 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) { try {
console.warn("⚠️ API key is not set. Skipping Safe Browsing check."); const response = await chrome.runtime.sendMessage({
return false; 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}`, { // Sanitize warning template to prevent XSS
method: "POST", function sanitizeWarningTemplate(template, appName) {
headers: { "Content-Type": "application/json" }, const div = document.createElement('div');
body: JSON.stringify({ div.textContent = template.replace("{app}", appName);
client: { clientId: "enterprise-app-protection", clientVersion: "1.0" }, return div.textContent;
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;
} }
// ✅ Scan page content for suspicious links // ✅ Scan page content for suspicious links
function analyzePageContent() { async function analyzePageContent() {
if (!isDomainsLoaded || !domainsDB || Object.keys(domainsDB).length === 0) { const startTime = performanceMonitor.startTimer();
console.warn("⚠️ domainsDB is not ready yet. Skipping analysis.");
return; 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)"); const links = document.querySelectorAll("a:not(.checked-by-extension)");
let newFlaggedLinks = new Set(); let newFlaggedLinks = new Set();
let processedLinks = 0;
links.forEach(link => { // Process links in batches of 50
try { const batchSize = 50;
// ✅ Validate the URL before using it const batches = Array.from(links).reduce((acc, link, i) => {
let url; const batchIndex = Math.floor(i / batchSize);
try { if (!acc[batchIndex]) acc[batchIndex] = [];
url = new URL(link.href); acc[batchIndex].push(link);
} catch (e) { return acc;
console.warn("⚠️ Skipping invalid URL:", link.href); }, []);
return; // Stop processing this link
}
const domain = url.hostname.toLowerCase(); for (const batch of batches) {
const linkText = link.innerText.trim(); 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 // Validate the URL
link.classList.add("checked-by-extension"); let url;
try {
url = new URL(link.href);
} catch (e) {
console.warn("⚠️ Skipping invalid URL:", link.href);
return;
}
// ✅ Skip trusted domains const domain = url.hostname.toLowerCase();
if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) { const linkText = link.innerText.trim();
console.log("Skipping trusted domain:", domain); processedLinks++;
return;
}
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) let matchedApp = null;
for (const [appName, validDomains] of Object.entries(domainsDB)) { for (const [appName, validDomains] of Object.entries(domainsDB)) {
const regex = new RegExp(`\\b${appName}\\b`, "i"); // Ensure full-word match const regex = new RegExp(`\\b${appName}\\b`, "i");
if (regex.test(linkText)) { if (regex.test(linkText)) {
matchedApp = appName; // Keep the most specific match matchedApp = appName;
} }
} }
if (matchedApp && domainsDB[matchedApp]) { if (matchedApp && domainsDB[matchedApp]) {
const validDomains = domainsDB[matchedApp]; const validDomains = domainsDB[matchedApp];
const isValid = isValidDomain(domain, validDomains, trustedDomains); const isValid = isValidDomain(domain, validDomains, trustedDomains);
if (!isValid) { if (!isValid) {
newFlaggedLinks.add(url.href); newFlaggedLinks.add(url.href);
const isUnsafe = await checkGoogleSafeBrowsing(url.href);
// Cache the result
cacheManager.set(link.href, {
isUnsafe,
appName: matchedApp
});
// ✅ Add warning immediately addWarningToLink(link, matchedApp, isUnsafe);
const warning = document.createElement("div"); }
warning.classList.add("warning-alert"); }
warning.style.cssText = ` } catch (e) {
background: #fff3cd; console.error("Unexpected error processing link:", e);
color: #856404; }
padding: 10px; }));
border: 1px solid #ffeeba;
border-radius: 4px;
margin: 5px 0;
font-size: 14px;
`;
warning.textContent = `⚠️ ${warningTemplate.replace("{app}", matchedApp)}`; // Add a small delay between batches to prevent UI blocking
warning.setAttribute("title", `This link goes to ${domain} instead of an official ${matchedApp} domain.`); await new Promise(resolve => setTimeout(resolve, 50));
link.parentElement.insertBefore(warning, link.nextSibling); }
// Update warning if Google Safe Browsing confirms danger // Update metrics
checkGoogleSafeBrowsing(url.href).then(isUnsafe => { performanceMonitor.logMetric('linkChecks', processedLinks);
if (isUnsafe) { performanceMonitor.logMetric('processingTime', performanceMonitor.endTimer(startTime));
warning.textContent = `⚠️ This link is confirmed dangerous by Google Safe Browsing!`;
}
});
}
}
} catch (e) {
console.error("Unexpected error processing link:", e);
}
});
// Store flagged links without waiting for Safe Browsing API // Store flagged links with encryption
chrome.storage.local.get(["flaggedLinks", "totalSuspiciousLinks"], function (result) { if (newFlaggedLinks.size > 0) {
let existingLinks = new Set(result.flaggedLinks || []); const storageData = {
let totalCount = existingLinks.size; 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) { let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link));
totalCount += newLinksToAdd.length;
chrome.storage.local.set({ if (newLinksToAdd.length > 0) {
totalSuspiciousLinks: totalCount, totalCount += newLinksToAdd.length;
flaggedLinks: [...existingLinks, ...newLinksToAdd]
}, () => { const updatedData = {
chrome.runtime.sendMessage({ action: "updatePopup", flaggedLinks: [...existingLinks, ...newLinksToAdd], totalCount }); 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 // ✅ Load domains and start analysis once ready
function loadDomainsAndAnalyze() { 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 || {}; domainsDB = result.domainsDB || {};
trustedDomains = result.trustedDomains || []; trustedDomains = result.trustedDomains || [];
blockedDomains = result.blockedDomains || []; 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."; 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); console.log("✅ Domains database loaded:", domainsDB);
isDomainsLoaded = true;
analyzePageContent(); analyzePageContent();
}); });
} }
@@ -213,6 +366,7 @@ const enhancedAnalyzePageContent = withErrorHandling(debouncedAnalyzePageContent
// Update the observer to use the enhanced version // Update the observer to use the enhanced version
function observePageForLinks() { function observePageForLinks() {
const observer = new MutationObserver(() => enhancedAnalyzePageContent()); const observer = new MutationObserver(() => enhancedAnalyzePageContent());
window.pageObserver = observer; // Store for cleanup
observer.observe(document.body, { childList: true, subtree: true }); observer.observe(document.body, { childList: true, subtree: true });
@@ -222,7 +376,6 @@ function observePageForLinks() {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
if (iframeDoc) { if (iframeDoc) {
observer.observe(iframeDoc.body, { childList: true, subtree: true }); observer.observe(iframeDoc.body, { childList: true, subtree: true });
console.log("Observing links inside iframe:", iframe.src);
} }
} catch (e) { } catch (e) {
console.warn("Cannot access iframe due to cross-origin restrictions:", iframe.src); 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 window.iframeInterval = setInterval(observeIframes, 3000);
setInterval(observeIframes, 3000);
} }
// ✅ Initialize extension scanning // ✅ Initialize extension scanning
document.addEventListener("DOMContentLoaded", loadDomainsAndAnalyze); document.addEventListener("DOMContentLoaded", loadDomainsAndAnalyze);
window.addEventListener("load", loadDomainsAndAnalyze); window.addEventListener("load", loadDomainsAndAnalyze);
observePageForLinks(); 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);

View File

@@ -5,197 +5,361 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise App Protection - Domain Management</title> <title>Enterprise App Protection - Domain Management</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style> <style>
body { :root {
font-family: Arial, sans-serif; --primary-color: #2563eb;
background-color: #f8f9fa; --success-color: #059669;
--danger-color: #dc2626;
--warning-color: #d97706;
--background-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
}
* {
margin: 0; margin: 0;
padding: 20px; padding: 0;
display: flex;
justify-content: center;
}
.container {
max-width: 600px;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
font-size: 24px;
color: #333;
text-align: center;
margin-bottom: 15px;
}
h3 {
font-size: 18px;
color: #555;
margin-top: 20px;
}
p {
font-size: 14px;
color: #666;
}
textarea {
width: 100%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
height: 120px;
resize: vertical;
box-sizing: border-box; box-sizing: border-box;
} }
pre { body {
background: #f4f4f4; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding: 10px; background-color: var(--background-color);
border-radius: 5px; color: var(--text-primary);
font-size: 14px; line-height: 1.5;
overflow-x: auto; min-height: 100vh;
padding: 2rem 1rem;
} }
.button-container { .container {
max-width: 1000px;
width: 100%;
margin: 0 auto;
background: var(--surface-color);
padding: 2rem;
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
.header {
text-align: center; text-align: center;
margin-top: 20px; margin-bottom: 2rem;
}
h1 {
font-size: 1.875rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.description {
color: var(--text-secondary);
font-size: 0.875rem;
max-width: 600px;
margin: 0 auto;
}
.management-section {
margin-bottom: 2rem;
padding: 1.5rem;
background-color: var(--background-color);
border-radius: var(--radius-md);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
h2 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.5rem;
}
.domain-list {
display: grid;
gap: 1rem;
margin-bottom: 1rem;
}
.domain-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
background-color: var(--surface-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
transition: all 0.2s ease;
}
.domain-item:hover {
border-color: var(--primary-color);
box-shadow: var(--shadow-sm);
}
.domain-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.domain-name {
font-weight: 500;
color: var(--text-primary);
}
.domain-type {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
border-radius: var(--radius-sm);
background-color: var(--background-color);
color: var(--text-secondary);
}
.domain-actions {
display: flex;
gap: 0.5rem;
} }
button { button {
background-color: #007bff; padding: 0.5rem 1rem;
color: white;
border: none; border: none;
padding: 10px 20px; border-radius: var(--radius-sm);
font-size: 16px; font-size: 0.875rem;
border-radius: 5px; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.3s; transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
} }
button:hover { button:hover {
background-color: #0056b3; transform: translateY(-1px);
box-shadow: var(--shadow-sm);
} }
/* Styled Popup */ button:active {
.popup { transform: translateY(0);
}
.add-btn {
background-color: var(--primary-color);
color: white;
}
.remove-btn {
background-color: var(--danger-color);
color: white;
}
.edit-btn {
background-color: var(--warning-color);
color: white;
}
.modal {
display: none; display: none;
position: fixed; position: fixed;
top: 50%; top: 0;
left: 50%; left: 0;
transform: translate(-50%, -50%); right: 0;
background: white; bottom: 0;
padding: 20px; background-color: rgba(0, 0, 0, 0.5);
border-radius: 8px; align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); justify-content: center;
z-index: 1000; z-index: 1000;
width: 300px;
text-align: center;
} }
.popup-content { .modal.active {
font-size: 16px; display: flex;
color: #333;
} }
.popup button { .modal-content {
background-color: #007bff; background-color: var(--surface-color);
padding: 2rem;
border-radius: var(--radius-md);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-md);
}
.modal-header {
margin-bottom: 1.5rem;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
input, select {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
font-size: 0.875rem;
color: var(--text-primary);
background-color: var(--surface-color);
transition: all 0.2s ease;
}
input:focus, select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 2rem;
}
.cancel-btn {
background-color: var(--surface-color);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.save-btn {
background-color: var(--success-color);
color: white; color: white;
border: none;
padding: 8px 16px;
font-size: 14px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
} }
.popup button:hover { .search-bar {
background-color: #0056b3; margin-bottom: 1.5rem;
} }
.hidden { .search-input {
width: 100%;
padding: 0.75rem 1rem;
padding-left: 2.5rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
font-size: 0.875rem;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="%2364748b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>');
background-repeat: no-repeat;
background-position: 0.75rem center;
background-size: 1rem;
}
.status-message {
text-align: center;
padding: 1rem;
margin-top: 1rem;
border-radius: var(--radius-md);
font-size: 0.875rem;
display: none; display: none;
} }
.visible { .status-message.success {
display: block; background-color: #ecfdf5;
color: var(--success-color);
border: 1px solid #a7f3d0;
} }
/* Style the placeholder text to make it look distinct from user input */ .status-message.error {
input::placeholder, background-color: #fef2f2;
textarea::placeholder { color: var(--danger-color);
color: #aaa; border: 1px solid #fecaca;
/* Lighter gray color */
font-style: italic;
/* Makes it look like a hint */
opacity: 1;
/* Ensures it's fully visible */
}
/* Ensure actual input text is clear */
input,
textarea {
color: #333;
/* Dark text for actual input */
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
font-style: italic;
color: #333;
/* Make text italic */
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>Enterprise App Protection - Domain Management</h1> <div class="header">
<h1>Domain Management</h1>
<!-- Restored Instructions --> <p class="description">Manage trusted and blocked domains for enterprise application protection</p>
<h3>✅ How to Use</h3>
<ul>
<li>✅ Enter <strong>one domain per line</strong> (no commas or spaces).</li>
<li><strong>Do not include</strong> <code>https://</code> or <code>www.</code> (only enter the domain
name).</li>
<li>✅ You can add subdomains if needed (e.g., <code>sub.example.com</code>).</li>
<li><strong>Trusted Domains:</strong> These sites will <u>not</u> be flagged as unsafe.</li>
<li><strong>Blocked Domains:</strong> These sites will <u>always</u> be flagged, even if Google Safe
Browsing does not detect them.</li>
<li>🚨 <strong>Example Entries:</strong></li>
</ul>
<pre><em>
example.com
secure-site.org
sub.example.com
malicious-site.net
</em></pre>
<h3>✅ Trusted Domains (Safe List)</h3>
<textarea id="trusted" placeholder="Enter trusted domains here..."></textarea>
<h3>❌ Blocked Domains (Unsafe List)</h3>
<textarea id="blocked" placeholder="Enter blocked domains here..."></textarea>
<div class="button-container">
<button id="save">💾 Save Domains</button>
</div> </div>
<div class="management-section">
<div class="section-header">
<h2>🔒 Trusted Domains</h2>
<button class="add-btn" id="addTrustedDomain">
<span></span>
<span>Add Domain</span>
</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="trustedSearch" placeholder="Search trusted domains...">
</div>
<div class="domain-list" id="trustedDomainsList">
<!-- Trusted domains will be populated here -->
</div>
</div>
<div class="management-section">
<div class="section-header">
<h2>🚫 Blocked Domains</h2>
<button class="add-btn" id="addBlockedDomain">
<span></span>
<span>Add Domain</span>
</button>
</div>
<div class="search-bar">
<input type="text" class="search-input" id="blockedSearch" placeholder="Search blocked domains...">
</div>
<div class="domain-list" id="blockedDomainsList">
<!-- Blocked domains will be populated here -->
</div>
</div>
<div id="statusMessage" class="status-message"></div>
</div> </div>
<!-- Styled Popup --> <!-- Add/Edit Domain Modal -->
<div id="popupMessage" class="popup hidden"> <div class="modal" id="domainModal">
<div class="popup-content"> <div class="modal-content">
<p id="popupText">Changes saved successfully!</p> <div class="modal-header">
<button id="popupClose">OK</button> <h3 class="modal-title" id="modalTitle">Add Domain</h3>
</div>
<div class="form-group">
<label for="domainName">Domain Name</label>
<input type="text" id="domainName" placeholder="Enter domain name (e.g., example.com)">
</div>
<div class="form-group">
<label for="domainType">Domain Type</label>
<select id="domainType">
<option value="trusted">Trusted</option>
<option value="blocked">Blocked</option>
</select>
</div>
<div class="modal-actions">
<button class="cancel-btn" id="cancelDomain">Cancel</button>
<button class="save-btn" id="saveDomain">Save Domain</button>
</div>
</div> </div>
</div> </div>

View File

@@ -6,12 +6,12 @@
"permissions": [ "permissions": [
"storage", "storage",
"activeTab", "activeTab",
"alarms",
"scripting", "scripting",
"tabs" "alarms"
], ],
"host_permissions": [ "host_permissions": [
"https://raw.githubusercontent.com/*" "https://raw.githubusercontent.com/*",
"https://safebrowsing.googleapis.com/*"
], ],
"background": { "background": {
"service_worker": "background.js", "service_worker": "background.js",
@@ -19,7 +19,12 @@
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["<all_urls>"], "matches": [
"https://*.office.com/*",
"https://*.sharepoint.com/*",
"https://*.teams.microsoft.com/*",
"https://*.microsoft.com/*"
],
"js": ["config.js", "content.js"], "js": ["config.js", "content.js"],
"run_at": "document_idle" "run_at": "document_idle"
} }
@@ -39,10 +44,17 @@
}, },
"options_page": "options.html", "options_page": "options.html",
"content_security_policy": { "content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'" "extension_pages": "script-src 'self'; object-src 'self'; default-src 'self'",
"sandbox": "sandbox allow-scripts allow-forms allow-popups allow-modals; script-src 'self' 'unsafe-inline' 'unsafe-eval'; child-src 'self'"
}, },
"web_accessible_resources": [{ "web_accessible_resources": [{
"resources": ["icons/*"], "resources": ["icons/*"],
"matches": ["<all_urls>"] "matches": [
}] "https://*.office.com/*",
"https://*.sharepoint.com/*",
"https://*.teams.microsoft.com/*",
"https://*.microsoft.com/*"
]
}],
"minimum_chrome_version": "88"
} }

View File

@@ -4,136 +4,262 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise App Protection - Settings</title> <title>Enterprise App Protection - Settings</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style> <style>
body { :root {
font-family: Arial, sans-serif; --primary-color: #2563eb;
background-color: #f8f9fa; --success-color: #059669;
--background-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
}
* {
margin: 0; margin: 0;
padding: 20px; padding: 0;
display: flex; box-sizing: border-box;
justify-content: center; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--background-color);
color: var(--text-primary);
line-height: 1.5;
min-height: 100vh;
padding: 2rem 1rem;
} }
.container { .container {
max-width: 600px; max-width: 800px;
width: 100%; width: 100%;
background: #fff; margin: 0 auto;
padding: 20px; background: var(--surface-color);
border-radius: 8px; padding: 2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
.header {
text-align: center;
margin-bottom: 2rem;
} }
h1 { h1 {
font-size: 24px; font-size: 1.875rem;
color: #333; font-weight: 600;
text-align: center; color: var(--text-primary);
margin-bottom: 15px; margin-bottom: 0.5rem;
}
.settings-group {
margin-bottom: 2rem;
padding: 1.5rem;
background-color: var(--background-color);
border-radius: var(--radius-md);
} }
h3 { h3 {
font-size: 18px; font-size: 1.125rem;
color: #555; font-weight: 600;
margin-top: 20px; color: var(--text-primary);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
} }
p { p {
font-size: 14px; font-size: 0.875rem;
color: #666; color: var(--text-secondary);
margin-bottom: 1rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 0.5rem;
} }
input, textarea { input, textarea {
width: 100%; width: 100%;
padding: 10px; padding: 0.75rem;
margin-top: 5px; border: 1px solid var(--border-color);
border: 1px solid #ccc; border-radius: var(--radius-sm);
border-radius: 5px; font-size: 0.875rem;
font-size: 14px; color: var(--text-primary);
box-sizing: border-box; /* Ensures padding is included in width */ background-color: var(--surface-color);
transition: all 0.2s ease;
}
input:focus, textarea:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
input::placeholder, textarea::placeholder {
color: var(--text-secondary);
opacity: 0.7;
} }
textarea { textarea {
height: 120px; min-height: 100px;
resize: vertical; resize: vertical;
} }
.help-text {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.5rem;
}
a { a {
color: #007bff; color: var(--primary-color);
text-decoration: none; text-decoration: none;
font-weight: 500;
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
} }
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
}
.button-container { .button-container {
text-align: center; display: flex;
margin-top: 20px; justify-content: center;
gap: 1rem;
margin-top: 2rem;
} }
button { button {
background-color: #007bff; padding: 0.75rem 1.5rem;
color: white;
border: none; border: none;
padding: 10px 20px; border-radius: var(--radius-md);
font-size: 16px; font-size: 0.875rem;
border-radius: 5px; font-weight: 500;
cursor: pointer; cursor: pointer;
transition: background 0.3s; transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
} }
button:hover { button:hover {
background-color: #0056b3; transform: translateY(-1px);
box-shadow: var(--shadow-md);
} }
/* Style the placeholder text to make it look distinct from user input */
input::placeholder, textarea::placeholder {
color: #aaa; /* Lighter gray color */
font-style: italic; /* Makes it look like a hint */
opacity: 1; /* Ensures it's fully visible */
}
/* Ensure actual input text is clear */ button:active {
input, textarea { transform: translateY(0);
color: #333; /* Dark text for actual input */ }
}
.save-btn {
background-color: var(--primary-color);
color: white;
}
.reset-btn {
background-color: var(--surface-color);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
.status-message {
text-align: center;
padding: 1rem;
margin-top: 1rem;
border-radius: var(--radius-md);
font-size: 0.875rem;
display: none;
}
.status-message.success {
background-color: #ecfdf5;
color: var(--success-color);
border: 1px solid #a7f3d0;
}
.status-message.error {
background-color: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<h1>Enterprise App Protection - Settings</h1> <div class="header">
<h1>Enterprise App Protection</h1>
<p>Configure your extension settings</p>
</div>
<h3>🔑 Google Safe Browsing API Key</h3> <div class="settings-group">
<p> <h3>🔑 Google Safe Browsing API Key</h3>
To use Google Safe Browsing, you need an API key. <p>
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer"> To use Google Safe Browsing, you need an API key.
Click here to create a key <a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
</a> (you may need to enable the Safe Browsing API in Google Cloud Console). Get your API key here
</p> </a>
<input type="text" id="apiKey" placeholder="Enter your API key here" /> </p>
<div class="form-group">
<label for="apiKey">API Key</label>
<input type="text" id="apiKey" placeholder="Enter your API key" />
<p class="help-text">Required for checking URLs against Google's Safe Browsing database</p>
</div>
</div>
<h3>🌐 Domains Database URL</h3> <div class="settings-group">
<p>Enter the URL for the domains database (JSON format):</p> <h3>🌐 Domains Database</h3>
<input type="text" id="domainsDBURL" placeholder="Enter the URL here" value="https://raw.githubusercontent.com/rrpbergsma/EnterpriseAppProtection/refs/heads/main/domains.json" /> <p>Configure the source for your domains database</p>
<div class="form-group">
<label for="domainsDBURL">Database URL</label>
<input type="text" id="domainsDBURL" placeholder="Enter the URL for your domains database"
value="https://raw.githubusercontent.com/rrpbergsma/EnterpriseAppProtection/refs/heads/main/domains.json" />
<p class="help-text">URL must point to a valid JSON file containing domain information</p>
</div>
<h3>⏱️ Update Interval (hours)</h3> <div class="form-group">
<p>Enter the interval (in hours) to check for updates to the domains database:</p> <label for="updateInterval">Update Interval (hours)</label>
<input type="number" id="updateInterval" placeholder="Enter the update interval here" value="24" /> <input type="number" id="updateInterval" placeholder="Enter update interval" value="24" min="1" max="168" />
<p class="help-text">How often to check for updates to the domains database (1-168 hours)</p>
<h3>⚠️ Warning Message Template</h3> </div>
<p>Enter the template for the warning message (use <code>{app}</code> to represent the application name):</p> </div>
<input type="text" id="warningTemplate" placeholder="Enter the warning message template here" value="Warning: This link claims to be {app} but goes to an unofficial domain." />
<div class="settings-group">
<h3>⚠️ Warning Messages</h3>
<p>Customize how warnings are displayed to users</p>
<div class="form-group">
<label for="warningTemplate">Warning Message Template</label>
<input type="text" id="warningTemplate"
placeholder="Enter warning message template"
value="Warning: This link claims to be {app} but goes to an unofficial domain." />
<p class="help-text">Use {app} to represent the application name in your warning message</p>
</div>
</div>
<div class="button-container"> <div class="button-container">
<button id="save">💾 Save Settings</button> <button id="save" class="save-btn">
<span>💾</span>
<span>Save Settings</span>
</button>
<button id="reset" class="reset-btn">
<span></span>
<span>Reset to Defaults</span>
</button>
</div> </div>
<div id="statusMessage" class="status-message"></div>
</div> </div>
<script src="options.js"></script> <script src="options.js"></script>

View File

@@ -4,89 +4,219 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise App Protection</title> <title>Enterprise App Protection</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style> <style>
:root {
--primary-color: #2563eb;
--success-color: #059669;
--info-color: #0891b2;
--danger-color: #dc2626;
--background-color: #f8fafc;
--surface-color: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body { body {
font-family: Arial, sans-serif; font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding: 15px; padding: 1.25rem;
width: 280px; width: 320px;
background-color: #f8f9fa; background-color: var(--background-color);
color: var(--text-primary);
line-height: 1.5;
}
.header {
text-align: center; text-align: center;
border-radius: 8px; margin-bottom: 1.5rem;
} }
h2 { h2 {
margin-top: 0; font-size: 1.25rem;
color: #333; font-weight: 600;
font-size: 18px; color: var(--text-primary);
margin-bottom: 0.5rem;
} }
p {
font-size: 14px; .stats {
margin: 10px 0; background-color: var(--surface-color);
color: #555; border-radius: var(--radius-md);
padding: 1rem;
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
} }
.counter { .counter {
font-size: 16px; font-size: 1.5rem;
font-weight: bold; font-weight: 600;
color: #dc3545; color: var(--danger-color);
display: block;
margin: 0.5rem 0;
} }
.links-container {
background-color: var(--surface-color);
border-radius: var(--radius-md);
padding: 0.75rem;
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
max-height: 150px;
overflow-y: auto;
}
.links-container::-webkit-scrollbar {
width: 6px;
}
.links-container::-webkit-scrollbar-track {
background: var(--background-color);
border-radius: var(--radius-sm);
}
.links-container::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: var(--radius-sm);
}
ul { ul {
list-style: none; list-style: none;
padding: 0;
text-align: left;
max-height: 120px;
overflow-y: auto;
background: #fff;
border-radius: 5px;
padding: 5px;
box-shadow: inset 0 0 5px rgba(0,0,0,0.1);
} }
li { li {
font-size: 13px; font-size: 0.875rem;
padding: 5px; padding: 0.5rem;
border-bottom: 1px solid #eee; border-bottom: 1px solid var(--border-color);
color: var(--text-secondary);
} }
li:last-child { li:last-child {
border-bottom: none; border-bottom: none;
} }
.last-update {
font-size: 0.75rem;
color: var(--text-secondary);
text-align: center;
margin-bottom: 1rem;
}
.button-group {
display: grid;
gap: 0.75rem;
margin-bottom: 1rem;
}
button { button {
width: 100%; width: 100%;
padding: 10px; padding: 0.75rem;
margin-top: 10px;
border: none; border: none;
border-radius: 5px; border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer; cursor: pointer;
font-size: 14px; transition: all 0.2s ease;
font-weight: bold; display: flex;
} align-items: center;
.update-btn { justify-content: center;
background: #007bff; gap: 0.5rem;
color: white;
}
.settings-btn {
background: #28a745;
color: white;
}
.domains-btn {
background: #17a2b8;
color: white;
} }
button:hover { button:hover {
opacity: 0.8; transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
button:active {
transform: translateY(0);
}
.update-btn {
background-color: var(--primary-color);
color: white;
}
.settings-btn {
background-color: var(--success-color);
color: white;
}
.domains-btn {
background-color: var(--info-color);
color: white;
}
.reset-section {
text-align: center;
padding-top: 1rem;
border-top: 1px solid var(--border-color);
}
.reset-section h3 {
font-size: 0.875rem;
color: var(--text-secondary);
margin-bottom: 0.75rem;
}
#resetCounter {
background-color: var(--surface-color);
color: var(--text-secondary);
border: 1px solid var(--border-color);
}
#resetCounter:hover {
background-color: var(--background-color);
} }
</style> </style>
</head> </head>
<body> <body>
<h2>Enterprise App Protection</h2> <div class="header">
<p>Suspicious Links Detected: <span class="counter" id="suspiciousCount">0</span></p> <h2>Enterprise App Protection</h2>
</div>
<ul id="suspiciousLinks"></ul> <div class="stats">
<p id="lastUpdate">Last Updated: Never</p> <span>Suspicious Links Detected</span>
<button id="updateDB" class="update-btn">🔄 Update Database</button> <span class="counter" id="suspiciousCount">0</span>
<button id="settings" class="settings-btn">⚙️ Open Settings</button> </div>
<button id="manageDomains" class="domains-btn">🌐 Manage Domains</button>
<h3>🔄 Reset Suspicious Links Counter</h3> <div class="links-container">
<button id="resetCounter">♻️ Reset Counter</button> <ul id="suspiciousLinks"></ul>
</div>
<p class="last-update" id="lastUpdate">Last Updated: Never</p>
<div class="button-group">
<button id="updateDB" class="update-btn">
<span>🔄</span>
<span>Update Database</span>
</button>
<button id="settings" class="settings-btn">
<span>⚙️</span>
<span>Open Settings</span>
</button>
<button id="manageDomains" class="domains-btn">
<span>🌐</span>
<span>Manage Domains</span>
</button>
</div>
<div class="reset-section">
<h3>Reset Suspicious Links Counter</h3>
<button id="resetCounter">
<span>♻️</span>
<span>Reset Counter</span>
</button>
</div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>