- Implement manifest.json for Chrome extension with necessary permissions and background scripts. - Create options.html for user settings including Google Safe Browsing API key, domains database URL, update interval, and warning message template. - Develop options.js to handle loading and saving settings using Chrome storage. - Design popup.html to display suspicious links and provide options to update the database and manage domains. - Implement popup.js to manage interactions in the popup, including updating the database and resetting the suspicious links counter. - Add testsite.html for dynamic testing of the extension with both official and fake links.
240 lines
9.0 KiB
JavaScript
240 lines
9.0 KiB
JavaScript
// content.js
|
|
|
|
let SAFE_BROWSING_API_KEY = "";
|
|
let domainsDB = {};
|
|
let trustedDomains = [];
|
|
let blockedDomains = [];
|
|
let warningTemplate = "Warning: This link claims to be {app} but goes to an unofficial domain.";
|
|
let isDomainsLoaded = false;
|
|
|
|
// ✅ Get the top-level domain (TLD)
|
|
function getTopLevelDomain(hostname) {
|
|
const domainParts = hostname.split(".");
|
|
const knownTLDs = ["co.uk", "com.au", "gov.uk", "edu.au"];
|
|
|
|
if (domainParts.length > 2) {
|
|
const lastTwoParts = domainParts.slice(-2).join(".");
|
|
if (knownTLDs.includes(lastTwoParts)) {
|
|
return domainParts.slice(-3).join(".");
|
|
}
|
|
}
|
|
return domainParts.slice(-2).join(".");
|
|
}
|
|
|
|
// ✅ Check if a domain is valid
|
|
function isValidDomain(domain, validDomains, trustedDomains) {
|
|
const extractedTLD = getTopLevelDomain(domain);
|
|
|
|
if (trustedDomains.includes(domain) || trustedDomains.includes(extractedTLD)) {
|
|
return true; // User has explicitly marked this domain as safe
|
|
}
|
|
|
|
return validDomains.some(validDomain => {
|
|
const validTLD = getTopLevelDomain(validDomain);
|
|
return extractedTLD === validTLD;
|
|
});
|
|
}
|
|
|
|
// ✅ Check Google Safe Browsing API for dangerous links
|
|
async function checkGoogleSafeBrowsing(url) {
|
|
console.log("🔍 Checking Google Safe Browsing for:", url);
|
|
|
|
if (!SAFE_BROWSING_API_KEY) {
|
|
console.warn("⚠️ API key is not set. Skipping Safe Browsing check.");
|
|
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;
|
|
}
|
|
|
|
// ✅ 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;
|
|
}
|
|
|
|
const links = document.querySelectorAll("a:not(.checked-by-extension)");
|
|
let newFlaggedLinks = new Set();
|
|
|
|
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 domain = url.hostname.toLowerCase();
|
|
const linkText = link.innerText.trim();
|
|
|
|
// ✅ Mark link as processed
|
|
link.classList.add("checked-by-extension");
|
|
|
|
// ✅ Skip trusted domains
|
|
if (trustedDomains.includes(domain) || trustedDomains.includes(getTopLevelDomain(domain))) {
|
|
console.log("Skipping trusted domain:", domain);
|
|
return;
|
|
}
|
|
|
|
let matchedApp = null;
|
|
|
|
// ✅ 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
|
|
}
|
|
}
|
|
|
|
if (matchedApp && domainsDB[matchedApp]) {
|
|
const validDomains = domainsDB[matchedApp];
|
|
const isValid = isValidDomain(domain, validDomains, trustedDomains);
|
|
|
|
if (!isValid) {
|
|
newFlaggedLinks.add(url.href);
|
|
|
|
// ✅ 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;
|
|
`;
|
|
|
|
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);
|
|
|
|
// ✅ 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);
|
|
}
|
|
});
|
|
|
|
// ✅ 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;
|
|
|
|
let newLinksToAdd = [...newFlaggedLinks].filter(link => !existingLinks.has(link));
|
|
|
|
if (newLinksToAdd.length > 0) {
|
|
totalCount += newLinksToAdd.length;
|
|
|
|
chrome.storage.local.set({
|
|
totalSuspiciousLinks: totalCount,
|
|
flaggedLinks: [...existingLinks, ...newLinksToAdd]
|
|
}, () => {
|
|
chrome.runtime.sendMessage({ action: "updatePopup", flaggedLinks: [...existingLinks, ...newLinksToAdd], totalCount });
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// ✅ Load domains and start analysis once ready
|
|
function loadDomainsAndAnalyze() {
|
|
chrome.storage.local.get(["domainsDB", "trustedDomains", "blockedDomains", "safeBrowsingApiKey", "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);
|
|
analyzePageContent();
|
|
});
|
|
}
|
|
|
|
// Debounce function to limit how often a function can be called
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function executedFunction(...args) {
|
|
const later = () => {
|
|
clearTimeout(timeout);
|
|
func(...args);
|
|
};
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(later, wait);
|
|
};
|
|
}
|
|
|
|
// Enhanced error handling wrapper
|
|
function withErrorHandling(fn, errorMessage) {
|
|
return async (...args) => {
|
|
try {
|
|
return await fn(...args);
|
|
} catch (error) {
|
|
console.error(`${errorMessage}:`, error);
|
|
// You could add error reporting service here
|
|
return null;
|
|
}
|
|
};
|
|
}
|
|
|
|
// Debounced version of analyzePageContent
|
|
const debouncedAnalyzePageContent = debounce(analyzePageContent, 250);
|
|
|
|
// Enhanced version of analyzePageContent with better error handling
|
|
const enhancedAnalyzePageContent = withErrorHandling(debouncedAnalyzePageContent, "Error analyzing page content");
|
|
|
|
// Update the observer to use the enhanced version
|
|
function observePageForLinks() {
|
|
const observer = new MutationObserver(() => enhancedAnalyzePageContent());
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
function observeIframes() {
|
|
document.querySelectorAll("iframe").forEach((iframe) => {
|
|
try {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// ✅ Run iframe observer every 3 seconds for Office 365 dynamic content
|
|
setInterval(observeIframes, 3000);
|
|
}
|
|
|
|
// ✅ Initialize extension scanning
|
|
document.addEventListener("DOMContentLoaded", loadDomainsAndAnalyze);
|
|
window.addEventListener("load", loadDomainsAndAnalyze);
|
|
observePageForLinks(); |