import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import { collect } from '@themarkup/blacklight-collector'; import { readFileSync, existsSync, writeFileSync, mkdirSync, createWriteStream, readdir, unlink } from 'node:fs'; import path from 'path'; import { fileURLToPath } from 'url'; import loadTrackerDB from '@ghostery/trackerdb'; import { parse } from 'tldts'; import { chromium } from 'playwright'; import axios from 'axios'; import cron from 'node-cron'; import maxmind from 'maxmind'; import geoip from 'geoip-lite'; import piiFilter from 'pii-filter'; import { Mutex, withTimeout } from 'async-mutex'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 7860; app.set('trust proxy', 1); app.use(helmet({ contentSecurityPolicy: false, frameguard: false })); app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use('/api/', rateLimit({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, })); const MAXMIND_ACCOUNT_ID = process.env.Account_ID; const MAXMIND_LICENSE_KEY = process.env.License_key; let ghosteryDB = null; let ddgTrackerRadar = { domains: {} }; let tosdrCache = new Map(); let geoReader = null; let useMaxMind = false; const mutex = new Mutex(); const scanMutex = withTimeout(mutex, 5 * 60 * 1000); class BrowserPool { constructor() { this.browser = null; this.launching = false; this.waiters = []; this.requestCount = 0; this.maxRequestsBeforeRestart = 8; } async getBrowser() { if (this.requestCount >= this.maxRequestsBeforeRestart) { console.log('Restarting browser to free memory...'); await this.shutdown(); this.requestCount = 0; } if (this.browser) { try { await this.browser.version(); this.requestCount++; return this.browser; } catch { this.browser = null; } } if (this.launching) { await new Promise(r => { this.waiters.push(r); setTimeout(r, 8000); }); return this.getBrowser(); } this.launching = true; try { this.browser = await chromium.launch({ headless: true, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--disable-software-rasterizer', '--disable-extensions', '--memory-pressure-off', '--disable-blink-features=AutomationControlled', '--disable-background-timer-throttling', '--disable-renderer-backgrounding' ], protocolTimeout: 240000, }); console.log('Browser launched'); this.requestCount = 1; this.waiters.forEach(r => r()); this.waiters = []; } finally { this.launching = false; } return this.browser; } async newTab() { const browser = await this.getBrowser(); const context = await browser.newContext({ javaScriptEnabled: true, ignoreHTTPSErrors: true, userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', viewport: { width: 1280, height: 720 } }); const page = await context.newPage(); return { page, context }; } async closeTab(context) { await context.close().catch(() => {}); } async shutdown() { if (this.browser) { await this.browser.close().catch(() => {}); this.browser = null; } } } const pool = new BrowserPool(); async function initGhosteryDB() { try { const enginePath = path.join(__dirname, 'node_modules', '@ghostery', 'trackerdb', 'dist', 'trackerdb.engine'); if (existsSync(enginePath)) { const engine = readFileSync(enginePath); ghosteryDB = await loadTrackerDB(engine); console.log('Ghostery TrackerDB initialized'); } } catch (e) { console.warn('Ghostery TrackerDB initialization failed:', e.message); } } function loadDDGTrackerRadar() { try { const ddgPath = path.join(__dirname, 'data', 'ddg-tracker-radar.json'); if (existsSync(ddgPath)) { ddgTrackerRadar = JSON.parse(readFileSync(ddgPath, 'utf-8')); console.log(`DDG Tracker Radar: ${Object.keys(ddgTrackerRadar.domains).length} domains`); } } catch (e) { console.warn('DDG Tracker Radar load failed:', e.message); } } cron.schedule('0 0 * * *', async () => { await downloadDDGTrackerRadar(); if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) { await downloadMaxMindDatabase(); } }); async function downloadDDGTrackerRadar() { try { const response = await axios.get('https://downloads.vivaldi.com/ddg/tds-v2-current.json', { timeout: 30000 }); const data = response.data; const simplified = { domains: {} }; for (const [domain, info] of Object.entries(data.domains)) { simplified.domains[domain] = { owner: info.owner?.name || 'Unknown', prevalence: info.prevalence || 0, fingerprinting: info.fingerprinting || 0, category: info.categories?.[0] || 'unknown' }; } const dir = path.join(__dirname, 'data'); if (!existsSync(dir)) mkdirSync(dir); writeFileSync(path.join(dir, 'ddg-tracker-radar.json'), JSON.stringify(simplified)); ddgTrackerRadar = simplified; console.log('DDG Tracker Radar updated'); } catch (e) { console.error('DDG download failed:', e.message); } } async function initGeoDatabase() { if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) { try { const dbPath = path.join(__dirname, 'data', 'geo', 'GeoLite2-City.mmdb'); if (!existsSync(dbPath)) await downloadMaxMindDatabase(dbPath); if (existsSync(dbPath)) { geoReader = await maxmind.open(dbPath); useMaxMind = true; console.log('MaxMind GeoLite2-City loaded'); return; } } catch (e) { console.warn('MaxMind initialization failed, falling back to geoip-lite:', e.message); } } console.log('Using geoip-lite for Geo mapping'); } async function downloadMaxMindDatabase(dbPath) { const url = `https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz`; const dir = path.dirname(dbPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); console.log('Downloading MaxMind GeoLite2-City...'); const response = await axios.get(url, { responseType: 'stream', timeout: 120000 }); const tarPath = path.join(dir, 'GeoLite2-City.tar.gz'); const writer = createWriteStream(tarPath); response.data.pipe(writer); await new Promise((resolve, reject) => { writer.on('finish', resolve); writer.on('error', reject); }); console.warn('MaxMind DB downloaded but auto-extraction not implemented. Using geoip-lite instead.'); } function normalizeUrl(inputUrl) { let url = inputUrl.trim(); if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url; try { return new URL(url).toString(); } catch { return null; } } function getBaseDomain(hostname) { const parsed = parse(hostname); return parsed.domain || hostname; } async function getGhosteryInfo(domain) { if (!ghosteryDB) return null; try { let matches = await ghosteryDB.matchDomain(domain); if ((!matches || matches.length === 0) && domain !== getBaseDomain(domain)) { matches = await ghosteryDB.matchDomain(getBaseDomain(domain)); } if (matches?.[0]) { const m = matches[0]; return { name: m.pattern?.name || domain, category: m.pattern?.category || 'unknown', organization: m.pattern?.organization?.name || null }; } } catch (e) {} return null; } function getDDGInfo(domain) { const baseDomain = getBaseDomain(domain); return ddgTrackerRadar.domains[domain] || ddgTrackerRadar.domains[baseDomain] || null; } async function getTosdrGrade(url) { try { const hostname = new URL(url).hostname; if (tosdrCache.has(hostname)) return tosdrCache.get(hostname); const searchResponse = await axios.get(`https://api.tosdr.org/search/v5?query=${encodeURIComponent(hostname)}`, { timeout: 5000 }); const services = searchResponse.data?.services || []; if (services.length === 0) { tosdrCache.set(hostname, null); return null; } const serviceId = services[0].id; const detailResponse = await axios.get(`https://api.tosdr.org/service/v3?id=${serviceId}`, { timeout: 5000 }); const service = detailResponse.data; const result = { grade: service.rating || 'N/A', name: service.name, points: service.points?.length || 0, reviewed: service.is_comprehensively_reviewed || false }; tosdrCache.set(hostname, result); return result; } catch (e) { return null; } } async function takeScreenshot(url) { const { page, context } = await pool.newTab(); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(500); const buffer = await page.screenshot({ type: 'jpeg', quality: 50, fullPage: false, clip: { x: 0, y: 0, width: 1024, height: 768 } }); return `data:image/jpeg;base64,${buffer.toString('base64')}`; } catch (e) { console.error('Screenshot failed:', e.message); return null; } finally { await pool.closeTab(context); } } async function checkHiddenStorage(url) { const { page, context } = await pool.newTab(); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); return await page.evaluate(() => ({ localStorage: Object.entries(localStorage).map(([k, v]) => ({ key: k, value: v })), sessionStorage: Object.entries(sessionStorage).map(([k, v]) => ({ key: k, value: v })), indexedDB: !!window.indexedDB })); } catch (e) { console.error('Hidden storage check failed:', e.message); return null; } finally { await pool.closeTab(context); } } async function analyzeCookieConsent(url) { const { page, context } = await pool.newTab(); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(1500); return await page.evaluate(() => { const bodyText = document.body.innerHTML.toLowerCase(); const allButtons = Array.from(document.querySelectorAll('button, a[role="button"], [class*="btn"]')) .map(el => el.innerText?.toLowerCase().trim()) .filter(Boolean); const hasAccept = allButtons.some(t => /accept|agree|allow|ok|got it|i agree/.test(t)); const hasReject = allButtons.some(t => /reject|decline|refuse|no thanks|deny|opt out/.test(t)); const hasBanner = /cookie|gdpr|consent|ccpa/.test(bodyText); const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const preChecked = checkboxes.filter(el => el.checked).length; const darkPatterns = []; if (hasAccept && !hasReject) darkPatterns.push('no_reject_option'); if (preChecked > 0) darkPatterns.push('pre_checked_boxes'); const hasCustomize = allButtons.some(t => /customize|settings|manage|preferences/.test(t)); return { has_banner: hasBanner, has_accept: hasAccept, has_reject: hasReject, has_customize: hasCustomize, pre_checked_boxes: preChecked, total_checkboxes: checkboxes.length, dark_patterns: darkPatterns, gdpr_compliant: hasAccept && hasReject, dark_pattern_detected: darkPatterns.length > 0 }; }); } catch (e) { console.error('Cookie consent analysis failed:', e.message); return null; } finally { await pool.closeTab(context); } } async function collectFingerprintRisks(url) { const { page, context } = await pool.newTab(); try { await page.addInitScript(() => { const orig = HTMLCanvasElement.prototype.toDataURL; HTMLCanvasElement.prototype.toDataURL = function(...args) { window.__fp_canvas = true; return orig.apply(this, args); }; const origGetContext = HTMLCanvasElement.prototype.getContext; HTMLCanvasElement.prototype.getContext = function(type, ...args) { if (type === 'webgl' || type === 'webgl2') window.__fp_webgl = true; return origGetContext.apply(this, args); }; const OrigAudio = window.AudioContext || window.webkitAudioContext; if (OrigAudio) { const OrigClass = OrigAudio; window.AudioContext = window.webkitAudioContext = function(...args) { window.__fp_audio = true; return new OrigClass(...args); }; } const origGetBattery = navigator.getBattery; if (origGetBattery) { navigator.getBattery = function() { window.__fp_battery = true; return origGetBattery.call(navigator); }; } const origEnumerate = navigator.mediaDevices?.enumerateDevices; if (origEnumerate) { navigator.mediaDevices.enumerateDevices = function() { window.__fp_media = true; return origEnumerate.call(navigator.mediaDevices); }; } }); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(2000); const result = await page.evaluate(() => ({ canvas: !!window.__fp_canvas, webgl: !!window.__fp_webgl, audio: !!window.__fp_audio, battery: !!window.__fp_battery, mediaDevices: !!window.__fp_media, webrtc_available: !!window.RTCPeerConnection, fonts_api: !!document.fonts, device_memory: navigator.deviceMemory || null, hardware_concurrency: navigator.hardwareConcurrency || null, screen: { width: screen.width, height: screen.height, depth: screen.colorDepth }, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, languages: navigator.languages ? [...navigator.languages] : [] })); const risks = []; if (result.canvas) risks.push('canvas_fingerprinting'); if (result.webgl) risks.push('webgl_fingerprinting'); if (result.audio) risks.push('audio_fingerprinting'); if (result.battery) risks.push('battery_status_api'); if (result.mediaDevices) risks.push('media_device_enumeration'); if (result.webrtc_available) risks.push('webrtc_ip_leak_risk'); return { ...result, risks_detected: risks, risk_level: risks.length === 0 ? 'low' : risks.length <= 2 ? 'medium' : 'high', risk_count: risks.length }; } catch (e) { console.error('Fingerprint risk collection failed:', e.message); return null; } finally { await pool.closeTab(context); } } async function analyzePrivacyPolicy(url) { const { page, context } = await pool.newTab(); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(2000); const privacyLink = await page.evaluate(() => { const links = Array.from(document.querySelectorAll('a')); const privacyLink = links.find(a => a.innerText.toLowerCase().includes('privacy') || a.innerText.toLowerCase().includes('policy') ); return privacyLink ? privacyLink.href : null; }); let policyUrl = privacyLink; let analysisText = ''; if (policyUrl) { await page.goto(policyUrl, { waitUntil: 'domcontentloaded', timeout: 15000 }); await page.waitForTimeout(2000); analysisText = await page.evaluate(() => document.body.innerText.toLowerCase()); } else { analysisText = await page.evaluate(() => document.body.innerText.toLowerCase()); } const dataSaleMentions = analysisText.includes('sell') && (analysisText.includes('data') || analysisText.includes('information')); const thirdPartyShare = analysisText.includes('third party') || analysisText.includes('third-party') || analysisText.includes('partners'); const retentionMention = analysisText.match(/retain.*?(\d+)\s*(day|month|year)/i); const dataRetention = retentionMention ? `${retentionMention[1]} ${retentionMention[2]}` : 'Not specified'; return { has_privacy_link: !!privacyLink, privacy_policy_url: policyUrl || null, data_sale_indicated: dataSaleMentions, third_party_sharing: thirdPartyShare, data_retention_period: dataRetention, analyzed_text_length: analysisText.length, source: policyUrl ? 'privacy_policy_page' : 'homepage' }; } catch (e) { console.error('Privacy policy analysis failed:', e.message); return null; } finally { await pool.closeTab(context); } } async function trackRedirectChain(url) { const TRACKING_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'mc_eid', 'ref', 'affiliate', 'clickid', 'adid', 'msclkid', 'twclid', '_ga', 'igshid']; const chain = []; let current = url; for (let i = 0; i < 12; i++) { try { const response = await axios.get(current, { maxRedirects: 0, validateStatus: s => s < 500, timeout: 5000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); let foundParams = []; try { const urlObj = new URL(current); foundParams = TRACKING_PARAMS.filter(p => urlObj.searchParams.has(p)); } catch {} chain.push({ url: current, status: response.status, tracking_params: foundParams, is_tracker: foundParams.length > 0 }); const location = response.headers.location; if (!location) break; current = location.startsWith('http') ? location : new URL(location, current).toString(); } catch (e) { break; } } const allTrackingParams = [...new Set(chain.flatMap(c => c.tracking_params))]; return { chain, total_redirects: Math.max(0, chain.length - 1), has_tracking: chain.some(c => c.tracking_params.length > 0), tracking_params_found: allTrackingParams, risk_level: allTrackingParams.length === 0 ? 'low' : allTrackingParams.length <= 2 ? 'medium' : 'high' }; } async function performBlacklightScan(url) { const tmpDir = path.join(__dirname, 'bl-tmp'); if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }); try { const options = { inUrl: url, blTests: [ 'cookies', 'third_party_trackers', 'fb_pixel_events', 'canvas_fingerprinters', 'canvas_font_fingerprinters', 'key_logging', 'session_recorders', 'google_analytics_events', 'twitter_pixel', 'tiktok_pixel' ], numPages: 1, defaultWaitUntil: 'domcontentloaded', captureHar: true, saveScreenshots: false, headless: true, defaultTimeout: 90000, extraChromiumArgs: [ '--disable-blink-features=AutomationControlled', '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu', '--ignore-certificate-errors' ], puppeteerOptions: { protocolTimeout: 240000, }, }; const result = await collect(url, options); try { const files = await readdir(tmpDir); await Promise.all(files.map(f => unlink(path.join(tmpDir, f)).catch(() => {}))); } catch (e) {} return result; } catch (e) { console.error('Blacklight scan failed:', e.message); return { hosts: {}, cookies: [], error: e.message }; } } async function performSecurityCheck(url) { try { const https = await import('node:https'); const { hostname } = new URL(url); const cert = await new Promise((resolve, reject) => { const req = https.request({ hostname, port: 443, method: 'HEAD', timeout: 5000 }, (res) => { resolve(res.socket.getPeerCertificate()); }); req.on('error', reject); req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); }); req.end(); }); const valid = cert && Object.keys(cert).length > 0; const issuer = cert.issuer?.O || cert.issuer?.CN || 'Unknown'; const expires = cert.valid_to ? new Date(cert.valid_to) : null; const daysRemaining = expires ? Math.floor((expires - Date.now()) / (1000 * 60 * 60 * 24)) : 0; return { ssl: { valid, issuer, expires_in_days: daysRemaining, protocol: 'TLS', grade: valid ? (daysRemaining > 30 ? 'A' : 'B') : 'F' } }; } catch (e) { return { ssl: { valid: false, error: e.message } }; } } function calculatePrivacyScore(blacklight, enrichedTrackers, tosdrGrade, security, cookieConsent, fingerprintRisks, redirectChain, privacyPolicy) { let score = 100; score -= Math.min(enrichedTrackers.length * 4, 30); if (blacklight?.canvasFingerprinters?.length) score -= 15; if (blacklight?.canvasFontFingerprinters?.length) score -= 10; if (fingerprintRisks?.risk_count > 0) score -= Math.min(fingerprintRisks.risk_count * 5, 20); const thirdPartyCookies = blacklight?.cookies?.filter(c => c.thirdParty)?.length || 0; score -= Math.min(thirdPartyCookies * 5, 20); if (blacklight?.sessionRecorders?.length > 0) score -= 10; if (blacklight?.keyLogging?.length > 0) score -= 15; if (!security?.ssl?.valid) score -= 20; if (cookieConsent?.dark_pattern_detected) score -= 10; if (cookieConsent?.has_banner && !cookieConsent?.gdpr_compliant) score -= 5; if (redirectChain?.has_tracking) score -= Math.min(redirectChain.tracking_params_found.length * 3, 10); if (privacyPolicy) { if (!privacyPolicy.has_privacy_link) score -= 5; if (privacyPolicy.data_sale_indicated) score -= 10; if (privacyPolicy.third_party_sharing) score -= 5; } if (tosdrGrade) { if (tosdrGrade.grade === 'A') score += 5; else if (tosdrGrade.grade === 'B') score += 2; else if (tosdrGrade.grade === 'D') score -= 5; else if (tosdrGrade.grade === 'E') score -= 10; } score = Math.max(0, Math.min(100, Math.round(score))); let grade; if (score >= 95) grade = 'A+'; else if (score >= 85) grade = 'A'; else if (score >= 75) grade = 'B'; else if (score >= 65) grade = 'C'; else if (score >= 55) grade = 'D'; else grade = 'F'; return { score, grade }; } app.get('/api/scan', async (req, res) => { const url = normalizeUrl(req.query.url); if (!url) { res.status(400).send('Invalid URL'); return; } res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); const sendEvent = (event, data) => { res.write(`event: ${event}\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }; sendEvent('start', { url, message: 'Scan started...' }); try { await scanMutex.runExclusive(async () => { const startTime = Date.now(); const results = { screenshot: null, hiddenStorage: null, cookieConsent: null, fingerprint: null, redirectChain: null, blacklight: null, tosdr: null, security: null, privacyPolicy: null }; const tools = [ { name: 'screenshot', fn: () => takeScreenshot(url), onStart: () => sendEvent('progress', { step: 'screenshot', status: 'started' }), onDone: (data) => { results.screenshot = data; sendEvent('progress', { step: 'screenshot', status: 'completed' }); sendEvent('step_result', { step: 'screenshot', result: data }); } }, { name: 'hidden_storage', fn: () => checkHiddenStorage(url), onStart: () => sendEvent('progress', { step: 'hidden_storage', status: 'started' }), onDone: (data) => { results.hiddenStorage = data; sendEvent('progress', { step: 'hidden_storage', status: 'completed' }); sendEvent('step_result', { step: 'hidden_storage', result: data }); } }, { name: 'cookie_consent', fn: () => analyzeCookieConsent(url), onStart: () => sendEvent('progress', { step: 'cookie_consent', status: 'started' }), onDone: (data) => { results.cookieConsent = data; sendEvent('progress', { step: 'cookie_consent', status: 'completed' }); sendEvent('step_result', { step: 'cookie_consent', result: data }); } }, { name: 'fingerprint_risks', fn: () => collectFingerprintRisks(url), onStart: () => sendEvent('progress', { step: 'fingerprint_risks', status: 'started' }), onDone: (data) => { results.fingerprint = data; sendEvent('progress', { step: 'fingerprint_risks', status: 'completed' }); sendEvent('step_result', { step: 'fingerprint_risks', result: data }); } }, { name: 'redirect_chain', fn: () => trackRedirectChain(url), onStart: () => sendEvent('progress', { step: 'redirect_chain', status: 'started' }), onDone: (data) => { results.redirectChain = data; sendEvent('progress', { step: 'redirect_chain', status: 'completed' }); sendEvent('step_result', { step: 'redirect_chain', result: data }); } }, { name: 'blacklight_scan', fn: () => performBlacklightScan(url), onStart: () => sendEvent('progress', { step: 'blacklight_scan', status: 'started' }), onDone: (data) => { results.blacklight = data; sendEvent('progress', { step: 'blacklight_scan', status: 'completed' }); sendEvent('step_result', { step: 'blacklight_scan', result: data }); } }, { name: 'tosdr', fn: () => getTosdrGrade(url), onStart: () => sendEvent('progress', { step: 'tosdr', status: 'started' }), onDone: (data) => { results.tosdr = data; sendEvent('progress', { step: 'tosdr', status: 'completed' }); sendEvent('step_result', { step: 'tosdr', result: data }); } }, { name: 'security', fn: () => performSecurityCheck(url), onStart: () => sendEvent('progress', { step: 'security', status: 'started' }), onDone: (data) => { results.security = data; sendEvent('progress', { step: 'security', status: 'completed' }); sendEvent('step_result', { step: 'security', result: data }); } }, { name: 'privacy_policy', fn: () => analyzePrivacyPolicy(url), onStart: () => sendEvent('progress', { step: 'privacy_policy', status: 'started' }), onDone: (data) => { results.privacyPolicy = data; sendEvent('progress', { step: 'privacy_policy', status: 'completed' }); sendEvent('step_result', { step: 'privacy_policy', result: data }); } } ]; const toolPromises = tools.map(tool => { tool.onStart(); return tool.fn() .then(data => { tool.onDone(data); return data; }) .catch(err => { console.error(`${tool.name} failed:`, err.message); tool.onDone(null); sendEvent('progress', { step: tool.name, status: 'failed', error: err.message }); sendEvent('step_result', { step: tool.name, error: err.message }); return null; }); }); await Promise.all(toolPromises); const blacklightData = results.blacklight || { hosts: {}, cookies: [] }; const thirdPartyDomains = [ ...(blacklightData.hosts?.thirdParty || []), ...(blacklightData.hosts?.requests?.third_party || []) ]; const uniqueDomains = [...new Set(thirdPartyDomains)]; const enrichedTrackers = []; for (const domain of uniqueDomains) { try { const ddgInfo = getDDGInfo(domain); const ghosteryInfo = await getGhosteryInfo(domain); enrichedTrackers.push({ domain, owner: ghosteryInfo?.organization || ddgInfo?.owner || getBaseDomain(domain), category: ghosteryInfo?.category || ddgInfo?.category || 'unknown', prevalence: ddgInfo?.prevalence || 0 }); } catch (e) { enrichedTrackers.push({ domain, owner: getBaseDomain(domain), category: 'unknown', prevalence: 0 }); } } const uniqueIps = [...new Set((blacklightData.hosts?.requests?.third_party || []).map(r => r.ip_addr).filter(Boolean))]; const geoDestinations = []; for (const ip of uniqueIps) { try { let geoData = null; if (useMaxMind && geoReader) { const mmGeo = geoReader.get(ip); if (mmGeo) { geoData = { ip, country: mmGeo.country?.names?.en || 'Unknown', city: mmGeo.city?.names?.en || 'Unknown', latitude: mmGeo.location?.latitude, longitude: mmGeo.location?.longitude }; } } if (!geoData) { const geoLite = geoip.lookup(ip); if (geoLite) { geoData = { ip, country: geoLite.country, city: geoLite.city, latitude: geoLite.ll?.[0] || null, longitude: geoLite.ll?.[1] || null }; } } if (geoData) geoDestinations.push(geoData); } catch (e) {} } const leakageAlerts = []; for (const r of (blacklightData.hosts?.requests?.third_party || [])) { if ((r.method === 'POST' || r.method === 'PUT') && r.body) { try { const detected = piiFilter.detect(r.body); if (detected?.length > 0) { leakageAlerts.push({ severity: 'high', destination: r.url, method: r.method, types: detected.map(p => p.type), message: 'Potential PII detected in request to third-party domain.' }); } } catch (e) {} } } const securityData = results.security || { ssl: { valid: false } }; const { score, grade } = calculatePrivacyScore( blacklightData, enrichedTrackers, results.tosdr, securityData, results.cookieConsent, results.fingerprint, results.redirectChain, results.privacyPolicy ); const finalResult = { success: true, url, final_url: blacklightData.uri_dest || url, scan_time_sec: (Date.now() - startTime) / 1000, privacy_score: { score, grade }, trackers: { count: enrichedTrackers.length, list: enrichedTrackers.slice(0, 20) }, cookies: { total: blacklightData.cookies?.length || 0, third_party: blacklightData.cookies?.filter(c => c.thirdParty)?.length || 0 }, fingerprinting: { canvas: !!(blacklightData.canvasFingerprinters?.length), fonts: !!(blacklightData.canvasFontFingerprinters?.length), live_risks: results.fingerprint }, session_recording: !!(blacklightData.sessionRecorders?.length), key_logging: !!(blacklightData.keyLogging?.length), hidden_storage: results.hiddenStorage ? { localStorage: results.hiddenStorage.localStorage?.length || 0, sessionStorage: results.hiddenStorage.sessionStorage?.length || 0, indexedDB: results.hiddenStorage.indexedDB } : null, cookie_consent: results.cookieConsent, redirect_chain: results.redirectChain, security: securityData, tosdr: results.tosdr, privacy_policy_analysis: results.privacyPolicy, geo_mapping: { data_destinations: geoDestinations }, leakage_detection: { alerts: leakageAlerts }, screenshot: results.screenshot, raw: blacklightData }; sendEvent('result', finalResult); sendEvent('end', { message: 'Scan completed.' }); }); } catch (e) { console.error('Live scan error:', e); sendEvent('error', { error: e.message }); } finally { res.end(); } }); app.get('/health', (req, res) => res.json({ status: 'ok' })); (async () => { const dirs = [path.join(__dirname, 'data'), path.join(__dirname, 'bl-tmp')]; for (const dir of dirs) { if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); } await initGhosteryDB(); loadDDGTrackerRadar(); await initGeoDatabase(); if (Object.keys(ddgTrackerRadar.domains).length === 0) { await downloadDDGTrackerRadar(); } app.listen(PORT, '0.0.0.0', () => { console.log(`Private Eye Live API running on port ${PORT}`); }); })();