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");
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
@@ -98,3 +251,77 @@ if (DEBUG_MODE) {
}
});
}
// 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
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();
const links = document.querySelectorAll("a:not(.checked-by-extension)");
let newFlaggedLinks = new Set();
if (!isDomainsLoaded || !domainsDB || Object.keys(domainsDB).length === 0) {
console.warn("⚠️ domainsDB is not ready yet. Skipping analysis.");
return;
}
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
}
const links = document.querySelectorAll("a:not(.checked-by-extension)");
let newFlaggedLinks = new Set();
let processedLinks = 0;
const domain = url.hostname.toLowerCase();
const linkText = link.innerText.trim();
// 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;
}, []);
// ✅ Mark link as processed
link.classList.add("checked-by-extension");
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;
}
// ✅ Skip trusted domains
if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) {
console.log("Skipping trusted domain:", domain);
return;
}
// Validate the URL
let url;
try {
url = new URL(link.href);
} catch (e) {
console.warn("⚠️ Skipping invalid URL:", link.href);
return;
}
let matchedApp = null;
const domain = url.hostname.toLowerCase();
const linkText = link.innerText.trim();
processedLinks++;
// ✅ 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
}
}
// Skip trusted domains
if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) {
return;
}
if (matchedApp && domainsDB[matchedApp]) {
const validDomains = domainsDB[matchedApp];
const isValid = isValidDomain(domain, validDomains, trustedDomains);
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 (!isValid) {
newFlaggedLinks.add(url.href);
if (matchedApp && domainsDB[matchedApp]) {
const validDomains = domainsDB[matchedApp];
const isValid = isValidDomain(domain, validDomains, trustedDomains);
// ✅ 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;
`;
if (!isValid) {
newFlaggedLinks.add(url.href);
const isUnsafe = await checkGoogleSafeBrowsing(url.href);
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);
// Cache the result
cacheManager.set(link.href, {
isUnsafe,
appName: matchedApp
});
// ✅ 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);
}
});
addWarningToLink(link, matchedApp, isUnsafe);
}
}
} catch (e) {
console.error("Unexpected error processing link:", e);
}
}));
// ✅ 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;
// Add a small delay between batches to prevent UI blocking
await new Promise(resolve => setTimeout(resolve, 50));
}
let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link));
// Update metrics
performanceMonitor.logMetric('linkChecks', processedLinks);
performanceMonitor.logMetric('processingTime', performanceMonitor.endTimer(startTime));
if (newLinksToAdd.length > 0) {
totalCount += newLinksToAdd.length;
// Store flagged links with encryption
if (newFlaggedLinks.size > 0) {
const storageData = {
flaggedLinks: Array.from(newFlaggedLinks),
timestamp: Date.now()
};
chrome.storage.local.set({
totalSuspiciousLinks: totalCount,
flaggedLinks: [...existingLinks, ...newLinksToAdd]
}, () => {
chrome.runtime.sendMessage({ action: "updatePopup", flaggedLinks: [...existingLinks, ...newLinksToAdd], totalCount });
});
}
});
if (await encryption.validateData(storageData)) {
chrome.storage.local.get(["flaggedLinks", "totalSuspiciousLinks"], async function (result) {
let existingLinks = new Set(result.flaggedLinks || []);
let totalCount = existingLinks.size;
let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link));
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();
// 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 name="viewport" content="width=device-width, initial-scale=1.0">
<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>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
:root {
--primary-color: #2563eb;
--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;
padding: 20px;
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;
padding: 0;
box-sizing: border-box;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
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;
}
.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;
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 {
background-color: #007bff;
color: white;
padding: 0.5rem 1rem;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
border-radius: var(--radius-sm);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
button:hover {
background-color: #0056b3;
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* Styled Popup */
.popup {
button:active {
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;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
z-index: 1000;
width: 300px;
text-align: center;
}
.popup-content {
font-size: 16px;
color: #333;
.modal.active {
display: flex;
}
.popup button {
background-color: #007bff;
.modal-content {
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;
border: none;
padding: 8px 16px;
font-size: 14px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
.popup button:hover {
background-color: #0056b3;
.search-bar {
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;
}
.visible {
display: block;
.status-message.success {
background-color: #ecfdf5;
color: var(--success-color);
border: 1px solid #a7f3d0;
}
/* 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 */
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 */
.status-message.error {
background-color: #fef2f2;
color: var(--danger-color);
border: 1px solid #fecaca;
}
</style>
</head>
<body>
<div class="container">
<h1>Enterprise App Protection - Domain Management</h1>
<!-- Restored Instructions -->
<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 class="header">
<h1>Domain Management</h1>
<p class="description">Manage trusted and blocked domains for enterprise application protection</p>
</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>
<!-- Styled Popup -->
<div id="popupMessage" class="popup hidden">
<div class="popup-content">
<p id="popupText">Changes saved successfully!</p>
<button id="popupClose">OK</button>
<!-- Add/Edit Domain Modal -->
<div class="modal" id="domainModal">
<div class="modal-content">
<div class="modal-header">
<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>

View File

@@ -6,12 +6,12 @@
"permissions": [
"storage",
"activeTab",
"alarms",
"scripting",
"tabs"
"alarms"
],
"host_permissions": [
"https://raw.githubusercontent.com/*"
"https://raw.githubusercontent.com/*",
"https://safebrowsing.googleapis.com/*"
],
"background": {
"service_worker": "background.js",
@@ -19,7 +19,12 @@
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"matches": [
"https://*.office.com/*",
"https://*.sharepoint.com/*",
"https://*.teams.microsoft.com/*",
"https://*.microsoft.com/*"
],
"js": ["config.js", "content.js"],
"run_at": "document_idle"
}
@@ -39,10 +44,17 @@
},
"options_page": "options.html",
"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": [{
"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 name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise App Protection - Settings</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f9fa;
:root {
--primary-color: #2563eb;
--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;
padding: 20px;
display: flex;
justify-content: center;
padding: 0;
box-sizing: border-box;
}
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 {
max-width: 600px;
max-width: 800px;
width: 100%;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin: 0 auto;
background: var(--surface-color);
padding: 2rem;
border-radius: var(--radius-md);
box-shadow: var(--shadow-md);
}
.header {
text-align: center;
margin-bottom: 2rem;
}
h1 {
font-size: 24px;
color: #333;
text-align: center;
margin-bottom: 15px;
font-size: 1.875rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.settings-group {
margin-bottom: 2rem;
padding: 1.5rem;
background-color: var(--background-color);
border-radius: var(--radius-md);
}
h3 {
font-size: 18px;
color: #555;
margin-top: 20px;
font-size: 1.125rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
p {
font-size: 14px;
color: #666;
font-size: 0.875rem;
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 {
width: 100%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
box-sizing: border-box; /* Ensures padding is included in width */
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, 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 {
height: 120px;
min-height: 100px;
resize: vertical;
}
.help-text {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.5rem;
}
a {
color: #007bff;
color: var(--primary-color);
text-decoration: none;
font-weight: 500;
}
a:hover {
text-decoration: underline;
}
pre {
background: #f4f4f4;
padding: 10px;
border-radius: 5px;
font-size: 14px;
overflow-x: auto;
}
.button-container {
text-align: center;
margin-top: 20px;
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
button {
background-color: #007bff;
color: white;
padding: 0.75rem 1.5rem;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.3s;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
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 */
input, textarea {
color: #333; /* Dark text for actual input */
}
button:active {
transform: translateY(0);
}
.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>
</head>
<body>
<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>
<p>
To use Google Safe Browsing, you need an API key.
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
Click here to create a key
</a> (you may need to enable the Safe Browsing API in Google Cloud Console).
</p>
<input type="text" id="apiKey" placeholder="Enter your API key here" />
<div class="settings-group">
<h3>🔑 Google Safe Browsing API Key</h3>
<p>
To use Google Safe Browsing, you need an API key.
<a href="https://console.cloud.google.com/apis/credentials" target="_blank" rel="noopener noreferrer">
Get your API key here
</a>
</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>
<p>Enter the URL for the domains database (JSON format):</p>
<input type="text" id="domainsDBURL" placeholder="Enter the URL here" value="https://raw.githubusercontent.com/rrpbergsma/EnterpriseAppProtection/refs/heads/main/domains.json" />
<div class="settings-group">
<h3>🌐 Domains Database</h3>
<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>
<p>Enter the interval (in hours) to check for updates to the domains database:</p>
<input type="number" id="updateInterval" placeholder="Enter the update interval here" value="24" />
<h3>⚠️ Warning Message Template</h3>
<p>Enter the template for the warning message (use <code>{app}</code> to represent the application name):</p>
<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="form-group">
<label for="updateInterval">Update Interval (hours)</label>
<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>
</div>
</div>
<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">
<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 id="statusMessage" class="status-message"></div>
</div>
<script src="options.js"></script>

View File

@@ -4,89 +4,219 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise App Protection</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<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 {
font-family: Arial, sans-serif;
padding: 15px;
width: 280px;
background-color: #f8f9fa;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
padding: 1.25rem;
width: 320px;
background-color: var(--background-color);
color: var(--text-primary);
line-height: 1.5;
}
.header {
text-align: center;
border-radius: 8px;
margin-bottom: 1.5rem;
}
h2 {
margin-top: 0;
color: #333;
font-size: 18px;
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
p {
font-size: 14px;
margin: 10px 0;
color: #555;
.stats {
background-color: var(--surface-color);
border-radius: var(--radius-md);
padding: 1rem;
margin-bottom: 1rem;
box-shadow: var(--shadow-sm);
}
.counter {
font-size: 16px;
font-weight: bold;
color: #dc3545;
font-size: 1.5rem;
font-weight: 600;
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 {
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 {
font-size: 13px;
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 0.875rem;
padding: 0.5rem;
border-bottom: 1px solid var(--border-color);
color: var(--text-secondary);
}
li:last-child {
border-bottom: none;
}
button {
width: 100%;
padding: 10px;
margin-top: 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.update-btn {
background: #007bff;
color: white;
}
.settings-btn {
background: #28a745;
color: white;
}
.domains-btn {
background: #17a2b8;
color: white;
}
button:hover {
opacity: 0.8;
.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 {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
button:hover {
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>
</head>
<body>
<h2>Enterprise App Protection</h2>
<p>Suspicious Links Detected: <span class="counter" id="suspiciousCount">0</span></p>
<div class="header">
<h2>Enterprise App Protection</h2>
</div>
<ul id="suspiciousLinks"></ul>
<p id="lastUpdate">Last Updated: Never</p>
<button id="updateDB" class="update-btn">🔄 Update Database</button>
<button id="settings" class="settings-btn">⚙️ Open Settings</button>
<button id="manageDomains" class="domains-btn">🌐 Manage Domains</button>
<div class="stats">
<span>Suspicious Links Detected</span>
<span class="counter" id="suspiciousCount">0</span>
</div>
<h3>🔄 Reset Suspicious Links Counter</h3>
<button id="resetCounter">♻️ Reset Counter</button>
<div class="links-container">
<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>
</body>