Files
ShieldAI/packages/extension/src/content/index.ts
Michael Freno de0ddac65d Add ShieldAI browser extension with phishing & spam detection (FRE-4576)
- Extension package: Manifest V3, background service worker, content scripts
- Phishing detection engine with heuristic analysis (typosquatting, entropy, TLD, brand impersonation)
- Local URL caching layer (Storage API) for <100ms cached lookups
- Popup UI with protection status, stats, and phishing report button
- Options page for settings management (blocked/allowed domains, feature toggles)
- Server-side extension routes: URL check, phishing report, auth, stats, exposure check
- Tier-aware feature gating (Basic/Plus/Premium)
- 25 passing tests for phishing detection heuristics
- Declarative net request rules for known phishing patterns
- DarkWatch integration for credential exposure checks
- Firefox compatibility layer via build modes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-09 21:53:29 -04:00

142 lines
4.5 KiB
TypeScript

import { BackgroundMessage, MessageType, UrlCheckResult, UrlVerdict } from '../types';
let currentUrlVerdict: UrlVerdict | null = null;
let statusBar: HTMLElement | null = null;
chrome.runtime.onMessage.addListener(
(message: BackgroundMessage) => {
switch (message.type) {
case MessageType.CHECK_URL_RESPONSE: {
const result = message.payload as UrlCheckResult;
currentUrlVerdict = result.verdict;
updateStatusBar(result);
injectPageBanner(result);
highlightSuspiciousLinks(result);
break;
}
}
}
);
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.sync.get('shieldaiSettings', (data) => {
if (data.shieldaiSettings?.enabled !== false) {
requestUrlCheck();
}
});
});
function requestUrlCheck(): void {
const url = window.location.href;
if (url.startsWith('chrome://') || url.startsWith('chrome-extension://')) return;
chrome.runtime.sendMessage({ type: MessageType.CHECK_URL, payload: { url } }).catch(() => {});
}
function updateStatusBar(result: UrlCheckResult): void {
if (!statusBar) {
statusBar = document.createElement('div');
statusBar.id = 'shieldai-status-bar';
Object.assign(statusBar.style, {
position: 'fixed',
top: '0',
left: '0',
right: '0',
height: '3px',
zIndex: '2147483647',
transition: 'background-color 0.3s ease',
});
document.documentElement.insertBefore(statusBar, document.documentElement.firstChild);
}
const colors: Record<UrlVerdict, string> = {
[UrlVerdict.SAFE]: '#22c55e',
[UrlVerdict.SUSPICIOUS]: '#f59e0b',
[UrlVerdict.PHISHING]: '#ef4444',
[UrlVerdict.SPAM]: '#f97316',
[UrlVerdict.EXPOSED_CREDENTIALS]: '#a855f7',
[UrlVerdict.UNKNOWN]: '#6b7280',
};
statusBar.style.backgroundColor = colors[result.verdict] || colors[UrlVerdict.UNKNOWN];
statusBar.title = `ShieldAI: ${result.verdict} (${result.threats.length} threat${result.threats.length !== 1 ? 's' : ''})`;
}
function injectPageBanner(result: UrlCheckResult): void {
const existing = document.getElementById('shieldai-banner');
if (existing) existing.remove();
if (result.verdict === UrlVerdict.SAFE || result.verdict === UrlVerdict.UNKNOWN) return;
const banner = document.createElement('div');
banner.id = 'shieldai-banner';
banner.innerHTML = `
<div id="shieldai-banner-content">
<span class="shieldai-icon">🛡️</span>
<strong>ShieldAI:</strong> ${result.verdict.toUpperCase()}${result.threats[0]?.description || 'Potential threat detected'}
<button id="shieldai-dismiss" style="margin-left: 12px; cursor: pointer; background: none; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px;">Dismiss</button>
</div>
`;
Object.assign(banner.style, {
position: 'fixed',
top: '3px',
left: '0',
right: '0',
zIndex: '2147483646',
backgroundColor: result.verdict === UrlVerdict.PHISHING ? '#fef2f2' : '#fffbeb',
borderBottom: `2px solid ${result.verdict === UrlVerdict.PHISHING ? '#ef4444' : '#f59e0b'}`,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
fontSize: '13px',
color: '#374151',
});
const content = banner.querySelector('#shieldai-banner-content') as HTMLElement;
Object.assign(content.style, {
maxWidth: '800px',
margin: '0 auto',
padding: '8px 16px',
});
document.documentElement.insertBefore(banner, document.documentElement.firstChild.nextSibling);
banner.querySelector('#shieldai-dismiss')?.addEventListener('click', () => {
banner.remove();
});
}
function highlightSuspiciousLinks(result: UrlCheckResult): void {
if (result.verdict === UrlVerdict.SAFE) return;
const links = document.querySelectorAll('a[href]');
links.forEach((link) => {
const href = link.getAttribute('href');
if (!href) return;
try {
const linkDomain = new URL(href, window.location.href).hostname;
const pageDomain = window.location.hostname;
if (linkDomain !== pageDomain && !linkDomain.includes(pageDomain)) {
link.classList.add('shieldai-external-link');
link.title = `ShieldAI: External link → ${linkDomain}`;
}
} catch {
// Relative or malformed URL
}
});
const style = document.createElement('style');
style.id = 'shieldai-link-styles';
style.textContent = `
a.shieldai-external-link::after {
content: " ↗";
opacity: 0.5;
font-size: 0.8em;
}
`;
document.head.appendChild(style);
}
requestUrlCheck();