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:
2026-05-09 21:53:29 -04:00
parent e5294ec712
commit de0ddac65d
27 changed files with 2591 additions and 1 deletions

View File

@@ -0,0 +1,237 @@
import { UrlCheckResult, UrlVerdict, ThreatInfo, BackgroundMessage, MessageType, SubscriptionTier, PhishingReport } from '../types';
import { urlCache, CACHE_TTL } from './cache';
import { phishingDetector } from './phishing-detector';
import { settingsManager } from './settings';
import { shieldApiClient } from './api-client';
let threatsBlockedToday = 0;
let urlsCheckedToday = 0;
let lastThreat: ThreatInfo | null = null;
chrome.runtime.onInstalled.addListener(async () => {
await urlCache.loadFromStorage();
await settingsManager.load();
const stats = await chrome.storage.local.get('dailyStats');
if (stats.dailyStats && stats.dailyStats.date === new Date().toDateString()) {
threatsBlockedToday = stats.dailyStats.threatsBlocked;
urlsCheckedToday = stats.dailyStats.urlsChecked;
} else {
threatsBlockedToday = 0;
urlsCheckedToday = 0;
await saveDailyStats();
}
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((details) => {
chrome.storage.local.get('blockedRequests').then((data) => {
const blocked = data.blockedRequests || [];
blocked.push({ ruleId: details.ruleId, url: details.requestUrl, timestamp: Date.now() });
if (blocked.length > 100) blocked.shift();
chrome.storage.local.set({ blockedRequests: blocked });
});
});
});
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (changeInfo.status !== 'loading' || !tab.url) return;
const enabled = await settingsManager.isProtectionEnabled();
if (!enabled) return;
const url = tab.url;
if (url.startsWith('chrome://') || url.startsWith('chrome-extension://')) return;
await analyzeAndAct(url, tabId);
});
async function analyzeAndAct(url: string, tabId: number): Promise<void> {
urlsCheckedToday++;
const startTime = Date.now();
const allowed = await isShieldAIUrl(url);
if (allowed) return;
const blocked = await settingsManager.isDomainBlocked(extractDomain(url));
if (blocked) {
threatsBlockedToday++;
await showBlockedPage(tabId, url);
await saveDailyStats();
return;
}
const cached = await urlCache.get(url);
if (cached) {
broadcastResult(cached, tabId);
if (cached.verdict === UrlVerdict.PHISHING) {
const features = await settingsManager.getFeatures();
if (features.activeBlocking) {
threatsBlockedToday++;
await showBlockedPage(tabId, url);
} else {
showWarningNotification(cached);
}
}
await saveDailyStats();
return;
}
const heuristic = phishingDetector.analyzeUrl(url);
let result: UrlCheckResult = {
url,
domain: extractDomain(url),
verdict: heuristic.verdict,
confidence: heuristic.score / 100,
threats: heuristic.threats,
cached: false,
latencyMs: Date.now() - startTime,
timestamp: Date.now(),
};
const apiResult = await shieldApiClient.checkUrl(url);
if (apiResult && apiResult.threats.length > 0) {
result = apiResult;
}
const darkWatchEnabled = await settingsManager.get();
if (darkWatchEnabled.darkWatchEnabled) {
const exposure = await shieldApiClient.checkDomainExposure(result.domain);
if (exposure && exposure.exposed) {
result.threats.push({
type: 'credential_exposure' as any,
severity: 4,
source: 'darkwatch',
description: `Credentials for ${result.domain} found in ${exposure.sources.length} breach(es)`,
});
if (result.verdict === UrlVerdict.SAFE) {
result.verdict = UrlVerdict.EXPOSED_CREDENTIALS;
}
}
}
await urlCache.set(url, result);
broadcastResult(result, tabId);
if (result.verdict === UrlVerdict.PHISHING) {
const features = await settingsManager.getFeatures();
if (features.activeBlocking) {
threatsBlockedToday++;
await showBlockedPage(tabId, url);
} else {
showWarningNotification(result);
}
}
if (result.threats.length > 0) {
lastThreat = result.threats[0];
}
await saveDailyStats();
}
function broadcastResult(result: UrlCheckResult, tabId: number): void {
chrome.tabs.sendMessage(tabId, {
type: MessageType.CHECK_URL_RESPONSE,
payload: result,
}).catch(() => {});
}
async function showBlockedPage(tabId: number, url: string): Promise<void> {
const blockedUrl = chrome.runtime.getURL(`popup.html?blocked=${encodeURIComponent(url)}`);
await chrome.tabs.update(tabId, { url: blockedUrl });
}
function showWarningNotification(result: UrlCheckResult): void {
const showNotif = settingsManager.get().then(s => s.showNotifications);
Promise.resolve(showNotif).then((enabled) => {
if (!enabled) return;
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon48.png',
title: 'ShieldAI Warning',
message: `${result.verdict.toUpperCase()}: ${result.domain}`,
priority: result.verdict === UrlVerdict.PHISHING ? 2 : 0,
});
});
}
async function saveDailyStats(): Promise<void> {
await chrome.storage.local.set({
dailyStats: {
date: new Date().toDateString(),
threatsBlocked: threatsBlockedToday,
urlsChecked: urlsCheckedToday,
},
});
await urlCache.persistToStorage();
}
function extractDomain(url: string): string {
try {
return new URL(url).hostname;
} catch {
return url;
}
}
async function isShieldAIUrl(url: string): Promise<boolean> {
const settings = await settingsManager.get();
const domain = extractDomain(url);
const apiDomain = new URL(settings.apiBaseUrl).hostname;
return domain === apiDomain || await settingsManager.isDomainAllowed(domain);
}
chrome.runtime.onMessage.addListener(
(message: BackgroundMessage, sender, sendResponse) => {
handleMessage(message, sender).then((res) => {
if (res) sendResponse(res);
});
return true;
}
);
async function handleMessage(
message: BackgroundMessage,
sender: chrome.runtime.MessageSender
): Promise<Record<string, unknown> | void> {
switch (message.type) {
case MessageType.CHECK_URL: {
const url = message.payload?.url as string;
if (!url) return;
const tabId = sender.tab?.id || 0;
await analyzeAndAct(url, tabId);
break;
}
case MessageType.GET_SETTINGS:
return { settings: await settingsManager.get() };
case MessageType.UPDATE_SETTINGS:
return { settings: await settingsManager.update(message.payload as Partial<ExtensionSettings>) };
case MessageType.REPORT_PHISHING: {
const report = message.payload as PhishingReport;
const success = await shieldApiClient.submitPhishingReport(report);
return { success };
}
case MessageType.GET_POPUP_DATA:
return {
protectionEnabled: await settingsManager.isProtectionEnabled(),
tier: await settingsManager.getTier(),
threatsBlockedToday,
urlsCheckedToday,
lastThreat,
isLoggedIn: await settingsManager.isLoggedIn(),
};
case MessageType.TOGGLE_PROTECTION: {
const enabled = await settingsManager.toggleProtection();
return { enabled };
}
case MessageType.AUTH_LOGOUT: {
await settingsManager.update({ authToken: null, userId: null, tier: null });
return { success: true };
}
}
}