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>
This commit is contained in:
141
packages/extension/src/content/index.ts
Normal file
141
packages/extension/src/content/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user