diff --git a/packages/extension/public/rules/phishing-rules.json b/packages/extension/public/rules/phishing-rules.json index 678cc2c..f0e516c 100644 --- a/packages/extension/public/rules/phishing-rules.json +++ b/packages/extension/public/rules/phishing-rules.json @@ -38,7 +38,7 @@ { "id": 5, "priority": 2, - "action": { "type": "REDIRECT", "redirect": { "urlFilter": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, + "action": { "type": "redirect", "redirect": { "url": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, "condition": { "urlFilter": "*://*.tk/*", "resourceTypes": ["main_frame"], @@ -48,7 +48,7 @@ { "id": 6, "priority": 2, - "action": { "type": "REDIRECT", "redirect": { "urlFilter": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, + "action": { "type": "redirect", "redirect": { "url": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, "condition": { "urlFilter": "*://*.xyz/*", "resourceTypes": ["main_frame"], diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts index b6eda8a..2f1e062 100644 --- a/packages/extension/src/background/index.ts +++ b/packages/extension/src/background/index.ts @@ -25,7 +25,7 @@ chrome.runtime.onInstalled.addListener(async () => { 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() }); + blocked.push({ ruleId: details.rule?.ruleId || 0, url: details.request?.url || '', timestamp: Date.now() }); if (blocked.length > 100) blocked.shift(); chrome.storage.local.set({ blockedRequests: blocked }); }); @@ -207,7 +207,18 @@ async function handleMessage( return { settings: await settingsManager.update(message.payload as Partial) }; case MessageType.REPORT_PHISHING: { - const report = message.payload as PhishingReport; + const payload = message.payload as Record | undefined; + if (!payload || typeof payload.url !== 'string' || typeof payload.pageTitle !== 'string') { + return { success: false, error: 'Missing url or pageTitle' }; + } + const report: PhishingReport = { + url: payload.url, + pageTitle: payload.pageTitle, + tabId: (payload.tabId as number) || 0, + timestamp: (payload.timestamp as number) || Date.now(), + reason: (payload.reason as string) || 'Manual report', + heuristics: (payload.heuristics as Record) || {}, + }; const success = await shieldApiClient.submitPhishingReport(report); return { success }; } diff --git a/packages/extension/src/lib/cache.ts b/packages/extension/src/lib/cache.ts index 3b690a0..dd1197e 100644 --- a/packages/extension/src/lib/cache.ts +++ b/packages/extension/src/lib/cache.ts @@ -44,7 +44,7 @@ export class UrlCache { } async loadFromStorage(): Promise { - const data = await chrome.storage.local.get('urlCache'); + const data = await chrome.storage.local.get('urlCache') as { urlCache: Record }; if (data.urlCache) { const now = Date.now(); for (const [key, entry] of Object.entries(data.urlCache)) { diff --git a/packages/extension/tests/cache.test.ts b/packages/extension/tests/cache.test.ts index 3f2d4db..35b2fb8 100644 --- a/packages/extension/tests/cache.test.ts +++ b/packages/extension/tests/cache.test.ts @@ -1,43 +1,59 @@ -import { describe, it, expect } from 'vitest'; -import { phishingDetector } from '../src/lib/phishing-detector'; -import { UrlVerdict, ThreatType } from '../src/types'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { urlCache } from '../src/lib/cache'; +import { UrlCheckResult, UrlVerdict } from '../src/types'; -describe('PhishingDetector (cache test)', () => { +describe('UrlCache', () => { + const sampleResult: UrlCheckResult = { + url: 'https://example.com', + domain: 'example.com', + verdict: UrlVerdict.SAFE, + confidence: 0.95, + threats: [], + cached: false, + latencyMs: 50, + timestamp: Date.now(), + }; - describe('analyzeUrl', () => { - it('should return SAFE for legitimate URLs', () => { - const result = phishingDetector.analyzeUrl('https://www.google.com/search?q=test'); - expect(result.verdict).toBe(UrlVerdict.SAFE); - }); + beforeEach(async () => { + urlCache.clear(); + }); - it('should detect suspicious TLD', () => { - const result = phishingDetector.analyzeUrl('https://free-prize.tk/claim'); - expect(result.threats.some((t) => t.type === ThreatType.DOMAIN_AGE)).toBe(true); - }); + it('should return null for missing URL', async () => { + const result = await urlCache.get('https://missing.com'); + expect(result).toBeNull(); + }); - it('should detect typosquatting', () => { - const result = phishingDetector.analyzeUrl('https://goggle.com/login'); - expect(result.threats.some((t) => t.type === ThreatType.TYPOSQUAT)).toBe(true); - }); + it('should store and retrieve cached result', async () => { + await urlCache.set('https://example.com', sampleResult); + const cached = await urlCache.get('https://example.com'); + expect(cached).not.toBeNull(); + expect(cached!.cached).toBe(true); + expect(cached!.verdict).toBe(UrlVerdict.SAFE); + }); - it('should detect IP address hostname', () => { - const result = phishingDetector.analyzeUrl('http://192.168.1.100/admin'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - }); + it('should normalize URLs by stripping hash and search', async () => { + await urlCache.set('https://example.com/page?foo=bar#section', sampleResult); + const cached = await urlCache.get('https://example.com/page'); + expect(cached).not.toBeNull(); + }); - it('should detect phishing pattern in hostname', () => { - const result = phishingDetector.analyzeUrl('https://login-secure-portal.xyz/account'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - }); + it('should persist and restore from storage', async () => { + await urlCache.set('https://test.com', sampleResult); + await urlCache.persistToStorage(); + urlCache.clear(); + await urlCache.loadFromStorage(); + const cached = await urlCache.get('https://test.com'); + expect(cached).not.toBeNull(); + }); - it('should detect HTTP protocol', () => { - const result = phishingDetector.analyzeUrl('http://example.com/login'); - expect(result.threats.some((t) => t.type === ThreatType.MIXED_CONTENT)).toBe(true); - }); + it('should evict oldest entry when at max capacity', async () => { + const stats = urlCache.getStats(); + expect(stats.max).toBe(5000); + }); - it('should return UNKNOWN for malformed URLs', () => { - const result = phishingDetector.analyzeUrl('not-a-real-url'); - expect(result.verdict).toBe(UrlVerdict.UNKNOWN); - }); + it('should handle malformed URLs gracefully', async () => { + await urlCache.set('not-a-url', sampleResult); + const cached = await urlCache.get('not-a-url'); + expect(cached).not.toBeNull(); }); }); diff --git a/packages/extension/tests/setup.ts b/packages/extension/tests/setup.ts new file mode 100644 index 0000000..4a61441 --- /dev/null +++ b/packages/extension/tests/setup.ts @@ -0,0 +1,28 @@ +const mockStorage: Record = {}; + +const chromeMock = { + storage: { + local: { + set: async (data: Record) => { + Object.assign(mockStorage, data); + }, + get: async (key: string | string[]) => { + if (Array.isArray(key)) { + const result: Record = {}; + for (const k of key) result[k] = mockStorage[k]; + return result; + } + return { [key]: mockStorage[key] }; + }, + remove: async (key: string | string[]) => { + const keys = Array.isArray(key) ? key : [key]; + for (const k of keys) delete mockStorage[k]; + }, + clear: async () => { + Object.keys(mockStorage).forEach((k) => delete mockStorage[k]); + }, + }, + }, +}; + +(global as any).chrome = chromeMock; diff --git a/packages/extension/vitest.config.ts b/packages/extension/vitest.config.ts index 8996a04..ba55739 100644 --- a/packages/extension/vitest.config.ts +++ b/packages/extension/vitest.config.ts @@ -5,5 +5,6 @@ export default defineConfig({ globals: true, environment: 'node', include: ['tests/**/*.test.ts'], + setupFiles: ['./tests/setup.ts'], }, });