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:
237
packages/extension/src/background/index.ts
Normal file
237
packages/extension/src/background/index.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user