lljz66 commited on
Commit
debf6e0
·
verified ·
1 Parent(s): 1b53e56

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +467 -153
server.js CHANGED
@@ -14,6 +14,7 @@ import cron from 'node-cron';
14
  import maxmind from 'maxmind';
15
  import geoip from 'geoip-lite';
16
  import piiFilter from 'pii-filter';
 
17
 
18
  const __filename = fileURLToPath(import.meta.url);
19
  const __dirname = path.dirname(__filename);
@@ -25,7 +26,6 @@ app.set('trust proxy', 1);
25
  app.use(helmet({ contentSecurityPolicy: false, frameguard: false }));
26
  app.use(cors());
27
  app.use(express.json({ limit: '10mb' }));
28
-
29
  app.use('/api/', rateLimit({
30
  windowMs: 15 * 60 * 1000,
31
  max: 100,
@@ -33,18 +33,95 @@ app.use('/api/', rateLimit({
33
  legacyHeaders: false,
34
  }));
35
 
36
- // 环境变量
37
  const MAXMIND_ACCOUNT_ID = process.env.Account_ID;
38
  const MAXMIND_LICENSE_KEY = process.env.License_key;
39
 
40
- // 全局数据库变量
41
  let ghosteryDB = null;
42
  let ddgTrackerRadar = { domains: {} };
43
  let tosdrCache = new Map();
44
  let geoReader = null;
45
  let useMaxMind = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- // --- 初始化 Ghostery TrackerDB ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  async function initGhosteryDB() {
49
  try {
50
  const enginePath = path.join(__dirname, 'node_modules', '@ghostery', 'trackerdb', 'dist', 'trackerdb.engine');
@@ -58,7 +135,6 @@ async function initGhosteryDB() {
58
  }
59
  }
60
 
61
- // --- 加载 DuckDuckGo Tracker Radar 数据 ---
62
  function loadDDGTrackerRadar() {
63
  try {
64
  const ddgPath = path.join(__dirname, 'data', 'ddg-tracker-radar.json');
@@ -71,7 +147,6 @@ function loadDDGTrackerRadar() {
71
  }
72
  }
73
 
74
- // --- 定时任务:每天更新 DDG 和 Geo 数据库 ---
75
  cron.schedule('0 0 * * *', async () => {
76
  await downloadDDGTrackerRadar();
77
  if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) {
@@ -79,7 +154,6 @@ cron.schedule('0 0 * * *', async () => {
79
  }
80
  });
81
 
82
- // --- 下载 DuckDuckGo Tracker Radar 数据 ---
83
  async function downloadDDGTrackerRadar() {
84
  try {
85
  const response = await axios.get('https://downloads.vivaldi.com/ddg/tds-v2-current.json', { timeout: 30000 });
@@ -103,15 +177,11 @@ async function downloadDDGTrackerRadar() {
103
  }
104
  }
105
 
106
- // --- 初始化地理定位数据库 ---
107
  async function initGeoDatabase() {
108
- // 如果提供了 MaxMind 密钥,尝试使用 maxmind
109
  if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) {
110
  try {
111
  const dbPath = path.join(__dirname, 'data', 'geo', 'GeoLite2-City.mmdb');
112
- if (!existsSync(dbPath)) {
113
- await downloadMaxMindDatabase(dbPath);
114
- }
115
  if (existsSync(dbPath)) {
116
  geoReader = await maxmind.open(dbPath);
117
  useMaxMind = true;
@@ -122,16 +192,13 @@ async function initGeoDatabase() {
122
  console.warn('���️ MaxMind initialization failed, falling back to geoip-lite:', e.message);
123
  }
124
  }
125
- // 回退到 geoip-lite
126
  console.log('✅ Using geoip-lite for Geo mapping');
127
  }
128
 
129
- // --- 下载 MaxMind 数据库(如果需要)---
130
  async function downloadMaxMindDatabase(dbPath) {
131
  const url = `https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
132
  const dir = path.dirname(dbPath);
133
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
134
-
135
  console.log('📥 Downloading MaxMind GeoLite2-City...');
136
  const response = await axios.get(url, { responseType: 'stream', timeout: 120000 });
137
  const tarPath = path.join(dir, 'GeoLite2-City.tar.gz');
@@ -141,13 +208,10 @@ async function downloadMaxMindDatabase(dbPath) {
141
  writer.on('finish', resolve);
142
  writer.on('error', reject);
143
  });
144
-
145
- // 注意:需要解压 tar.gz 文件,此处简化处理,实际项目中建议使用 tar 库解压
146
- // 由于解压较为复杂,这里只下载,实际使用时如果无法解压,geoReader 将无法初始化,会自动回退到 geoip-lite
147
  console.warn('⚠️ MaxMind DB downloaded but auto-extraction not implemented. Using geoip-lite instead.');
148
  }
149
 
150
- // --- 工具函数 ---
151
  function normalizeUrl(inputUrl) {
152
  let url = inputUrl.trim();
153
  if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url;
@@ -159,7 +223,6 @@ function getBaseDomain(hostname) {
159
  return parsed.domain || hostname;
160
  }
161
 
162
- // --- 获取 Ghostery 追踪器信息 ---
163
  async function getGhosteryInfo(domain) {
164
  if (!ghosteryDB) return null;
165
  try {
@@ -179,29 +242,21 @@ async function getGhosteryInfo(domain) {
179
  return null;
180
  }
181
 
182
- // --- 获取 DuckDuckGo 追踪器信息 ---
183
  function getDDGInfo(domain) {
184
  const baseDomain = getBaseDomain(domain);
185
  return ddgTrackerRadar.domains[domain] || ddgTrackerRadar.domains[baseDomain] || null;
186
  }
187
 
188
- // --- 获取 ToS;DR 评级 ---
189
  async function getTosdrGrade(url) {
190
  try {
191
  const hostname = new URL(url).hostname;
192
  if (tosdrCache.has(hostname)) return tosdrCache.get(hostname);
193
-
194
  const searchResponse = await axios.get(`https://api.tosdr.org/search/v5?query=${encodeURIComponent(hostname)}`, { timeout: 5000 });
195
  const services = searchResponse.data?.services || [];
196
- if (services.length === 0) {
197
- tosdrCache.set(hostname, null);
198
- return null;
199
- }
200
-
201
  const serviceId = services[0].id;
202
  const detailResponse = await axios.get(`https://api.tosdr.org/service/v3?id=${serviceId}`, { timeout: 5000 });
203
  const service = detailResponse.data;
204
-
205
  const result = {
206
  grade: service.rating || 'N/A',
207
  name: service.name,
@@ -215,99 +270,305 @@ async function getTosdrGrade(url) {
215
  }
216
  }
217
 
218
- // --- 截图 ---
 
219
  async function takeScreenshot(url) {
220
- let browser = null;
221
  try {
222
- browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
223
- const page = await browser.newPage();
224
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
225
- await page.waitForTimeout(1000);
226
- const buffer = await page.screenshot({ type: 'jpeg', quality: 85, fullPage: false });
227
  return `data:image/jpeg;base64,${buffer.toString('base64')}`;
228
  } catch (e) {
229
  console.error('Screenshot failed:', e.message);
230
  return null;
231
  } finally {
232
- if (browser) await browser.close().catch(() => {});
233
  }
234
  }
235
 
236
- // --- Blacklight 扫描 ---
237
- async function performBlacklightScan(url) {
238
  try {
239
- const options = {
240
- inUrl: url,
241
- blTests: ['cookies', 'third_party_trackers', 'fb_pixel_events', 'canvas_fingerprinters', 'canvas_font_fingerprinters', 'key_logging', 'session_recorders', 'google_analytics_events', 'twitter_pixel', 'tiktok_pixel'],
242
- numPages: 1,
243
- defaultWaitUntil: 'networkidle2',
244
- captureHar: true,
245
- saveScreenshots: false,
246
- headless: true,
247
- defaultTimeout: 60000,
248
- extraChromiumArgs: ['--disable-blink-features=AutomationControlled','--no-sandbox','--disable-setuid-sandbox','--disable-dev-shm-usage','--disable-gpu','--ignore-certificate-errors']
249
- };
250
- return await collect(url, options);
251
  } catch (e) {
252
- console.error('Blacklight scan failed:', e.message);
253
- return { hosts: {}, cookies: [], error: e.message };
 
 
254
  }
255
  }
256
 
257
- // --- 隐藏存储检测 ---
258
- async function checkHiddenStorage(url) {
259
- let browser = null;
260
  try {
261
- browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] });
262
- const page = await browser.newPage();
263
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
264
- const storageData = await page.evaluate(() => {
265
- const ls = [];
266
- const ss = [];
267
- for (let i = 0; i < localStorage.length; i++) {
268
- const key = localStorage.key(i);
269
- ls.push({ key, value: localStorage.getItem(key) });
270
- }
271
- for (let i = 0; i < sessionStorage.length; i++) {
272
- const key = sessionStorage.key(i);
273
- ss.push({ key, value: sessionStorage.getItem(key) });
274
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  return {
276
- localStorage: ls,
277
- sessionStorage: ss,
278
- indexedDB: !!window.indexedDB
 
 
 
 
 
 
279
  };
280
  });
281
- return storageData;
282
  } catch (e) {
283
- console.error('Hidden storage check failed:', e.message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  return null;
285
  } finally {
286
- if (browser) await browser.close().catch(() => {});
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  }
288
  }
289
 
290
- // --- SSL 安全检查 ---
291
  async function performSecurityCheck(url) {
292
  try {
293
  const https = await import('node:https');
294
  const { hostname } = new URL(url);
295
-
296
  const cert = await new Promise((resolve, reject) => {
297
  const req = https.request({ hostname, port: 443, method: 'HEAD', timeout: 5000 }, (res) => {
298
- const cert = res.socket.getPeerCertificate();
299
- resolve(cert);
300
  });
301
  req.on('error', reject);
302
  req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
303
  req.end();
304
  });
305
-
306
  const valid = cert && Object.keys(cert).length > 0;
307
  const issuer = cert.issuer?.O || cert.issuer?.CN || 'Unknown';
308
  const expires = cert.valid_to ? new Date(cert.valid_to) : null;
309
  const daysRemaining = expires ? Math.floor((expires - Date.now()) / (1000 * 60 * 60 * 24)) : 0;
310
-
311
  return {
312
  ssl: {
313
  valid,
@@ -315,41 +576,55 @@ async function performSecurityCheck(url) {
315
  expires_in_days: daysRemaining,
316
  protocol: 'TLS',
317
  grade: valid ? (daysRemaining > 30 ? 'A' : 'B') : 'F'
318
- },
319
- headers: {}
320
  };
321
  } catch (e) {
322
  return { ssl: { valid: false, error: e.message } };
323
  }
324
  }
325
 
326
- // --- 隐私评分计算 ---
327
- function calculatePrivacyScore(blacklight, enrichedTrackers, tosdrGrade, security) {
328
  let score = 100;
329
-
330
- const trackerCount = enrichedTrackers.length;
331
- score -= Math.min(trackerCount * 4, 30);
332
-
333
- const hasFingerprinting = !!(blacklight?.canvasFingerprinters?.length) || !!(blacklight?.canvasFontFingerprinters?.length);
334
- if (hasFingerprinting) score -= 30;
335
-
 
 
 
 
 
336
  const thirdPartyCookies = blacklight?.cookies?.filter(c => c.thirdParty)?.length || 0;
337
  score -= Math.min(thirdPartyCookies * 5, 20);
338
-
 
339
  if (blacklight?.sessionRecorders?.length > 0) score -= 10;
340
- if (blacklight?.keyLogging?.length > 0) score -= 10;
341
-
 
342
  if (!security?.ssl?.valid) score -= 20;
343
-
 
 
 
 
 
 
 
 
344
  if (tosdrGrade) {
345
  if (tosdrGrade.grade === 'A') score += 5;
346
  else if (tosdrGrade.grade === 'B') score += 2;
347
  else if (tosdrGrade.grade === 'D') score -= 5;
348
  else if (tosdrGrade.grade === 'E') score -= 10;
349
  }
350
-
351
  score = Math.max(0, Math.min(100, Math.round(score)));
352
-
353
  let grade;
354
  if (score >= 95) grade = 'A+';
355
  else if (score >= 85) grade = 'A';
@@ -357,41 +632,65 @@ function calculatePrivacyScore(blacklight, enrichedTrackers, tosdrGrade, securit
357
  else if (score >= 65) grade = 'C';
358
  else if (score >= 55) grade = 'D';
359
  else grade = 'F';
360
-
361
  return { score, grade };
362
  }
363
 
364
- // ==================== API 端点 ====================
 
365
  app.get('/api/scan', async (req, res) => {
366
  const url = normalizeUrl(req.query.url);
367
  if (!url) return res.status(400).json({ error: 'Invalid URL' });
368
-
 
 
 
 
 
 
 
 
 
369
  const startTime = Date.now();
370
-
371
  try {
372
- // 并行执行所有任务
373
- const [screenshot, blacklight, tosdr, security, hiddenStorage] = await Promise.allSettled([
 
 
 
 
 
 
 
 
 
 
374
  takeScreenshot(url),
 
 
 
 
375
  performBlacklightScan(url),
376
  getTosdrGrade(url),
377
- performSecurityCheck(url),
378
- checkHiddenStorage(url)
379
  ]);
380
-
381
- const screenshotData = screenshot.status === 'fulfilled' ? screenshot.value : null;
382
- const blacklightData = blacklight.status === 'fulfilled' ? blacklight.value : { hosts: {}, cookies: [] };
383
- const tosdrData = tosdr.status === 'fulfilled' ? tosdr.value : null;
384
- const securityData = security.status === 'fulfilled' ? security.value : { ssl: { valid: false } };
385
- const hiddenData = hiddenStorage.status === 'fulfilled' ? hiddenStorage.value : null;
386
-
387
- // 提取第三方域名
 
 
 
388
  const thirdPartyDomains = [
389
  ...(blacklightData.hosts?.thirdParty || []),
390
  ...(blacklightData.hosts?.requests?.third_party || [])
391
  ];
392
  const uniqueDomains = [...new Set(thirdPartyDomains)];
393
-
394
- // 丰富追踪器数据
395
  const enrichedTrackers = [];
396
  for (const domain of uniqueDomains) {
397
  try {
@@ -404,19 +703,14 @@ app.get('/api/scan', async (req, res) => {
404
  prevalence: ddgInfo?.prevalence || 0
405
  });
406
  } catch (e) {
407
- enrichedTrackers.push({
408
- domain,
409
- owner: getBaseDomain(domain),
410
- category: 'unknown',
411
- prevalence: 0
412
- });
413
  }
414
  }
415
-
416
- // --- Geo Mapping ---
417
  const uniqueIps = [...new Set(
418
- blacklightData.hosts?.requests?.third_party
419
- ?.map(req => req.ip_addr).filter(ip => ip) || []
420
  )];
421
  const geoDestinations = [];
422
  for (const ip of uniqueIps) {
@@ -426,7 +720,7 @@ app.get('/api/scan', async (req, res) => {
426
  const mmGeo = geoReader.get(ip);
427
  if (mmGeo) {
428
  geoData = {
429
- ip: ip,
430
  country: mmGeo.country?.names?.en || 'Unknown',
431
  city: mmGeo.city?.names?.en || 'Unknown',
432
  latitude: mmGeo.location?.latitude,
@@ -438,30 +732,29 @@ app.get('/api/scan', async (req, res) => {
438
  const geoLite = geoip.lookup(ip);
439
  if (geoLite) {
440
  geoData = {
441
- ip: ip,
442
  country: geoLite.country,
443
  city: geoLite.city,
444
- latitude: geoLite.ll ? geoLite.ll[0] : null,
445
- longitude: geoLite.ll ? geoLite.ll[1] : null
446
  };
447
  }
448
  }
449
  if (geoData) geoDestinations.push(geoData);
450
  } catch (e) {}
451
  }
452
-
453
- // --- Leakage Detection ---
454
  const leakageAlerts = [];
455
- const thirdPartyRequests = blacklightData.hosts?.requests?.third_party || [];
456
- for (const req of thirdPartyRequests) {
457
- if ((req.method === 'POST' || req.method === 'PUT') && req.body) {
458
  try {
459
- const detected = piiFilter.detect(req.body);
460
- if (detected && detected.length > 0) {
461
  leakageAlerts.push({
462
  severity: 'high',
463
- destination: req.url,
464
- method: req.method,
465
  types: detected.map(p => p.type),
466
  message: 'Potential PII detected in request to third-party domain.'
467
  });
@@ -469,24 +762,33 @@ app.get('/api/scan', async (req, res) => {
469
  } catch (e) {}
470
  }
471
  }
472
-
473
- const { score, grade } = calculatePrivacyScore(blacklightData, enrichedTrackers, tosdrData, securityData);
474
-
475
- // 最终输出
 
 
 
 
476
  const result = {
477
  success: true,
478
  url,
479
  final_url: blacklightData.uri_dest || url,
480
  scan_time_sec: (Date.now() - startTime) / 1000,
481
  privacy_score: { score, grade },
482
- trackers: { count: enrichedTrackers.length, list: enrichedTrackers.slice(0, 20) },
 
 
 
 
483
  cookies: {
484
  total: blacklightData.cookies?.length || 0,
485
  third_party: blacklightData.cookies?.filter(c => c.thirdParty)?.length || 0
486
  },
487
  fingerprinting: {
488
  canvas: !!(blacklightData.canvasFingerprinters?.length),
489
- fonts: !!(blacklightData.canvasFontFingerprinters?.length)
 
490
  },
491
  session_recording: !!(blacklightData.sessionRecorders?.length),
492
  key_logging: !!(blacklightData.keyLogging?.length),
@@ -495,37 +797,49 @@ app.get('/api/scan', async (req, res) => {
495
  sessionStorage: hiddenData.sessionStorage?.length || 0,
496
  indexedDB: hiddenData.indexedDB
497
  } : null,
 
 
498
  security: securityData,
499
  tosdr: tosdrData,
500
- geo_mapping: {
501
- data_destinations: geoDestinations
502
- },
503
- leakage_detection: {
504
- alerts: leakageAlerts
505
- },
506
  screenshot: screenshotData,
507
  raw: blacklightData
508
  };
509
-
510
  res.json(result);
511
-
512
  } catch (e) {
513
  console.error('Scan error:', e);
514
  res.status(500).json({ success: false, error: e.message });
 
 
515
  }
516
  });
517
 
518
  app.get('/health', (req, res) => res.json({ status: 'ok' }));
519
  app.use(express.static('public'));
520
 
521
- // --- 启动服务器 ---
522
  (async () => {
 
 
 
 
 
 
 
 
 
 
523
  await initGhosteryDB();
524
  loadDDGTrackerRadar();
525
  await initGeoDatabase();
 
526
  if (Object.keys(ddgTrackerRadar.domains).length === 0) {
527
  await downloadDDGTrackerRadar();
528
  }
 
529
  app.listen(PORT, '0.0.0.0', () => {
530
  console.log(`🚀 Private Eye with Geo & Leak Detection running on ${PORT}`);
531
  });
 
14
  import maxmind from 'maxmind';
15
  import geoip from 'geoip-lite';
16
  import piiFilter from 'pii-filter';
17
+ import { Mutex } from 'async-mutex';
18
 
19
  const __filename = fileURLToPath(import.meta.url);
20
  const __dirname = path.dirname(__filename);
 
26
  app.use(helmet({ contentSecurityPolicy: false, frameguard: false }));
27
  app.use(cors());
28
  app.use(express.json({ limit: '10mb' }));
 
29
  app.use('/api/', rateLimit({
30
  windowMs: 15 * 60 * 1000,
31
  max: 100,
 
33
  legacyHeaders: false,
34
  }));
35
 
36
+ // ==================== ENV ====================
37
  const MAXMIND_ACCOUNT_ID = process.env.Account_ID;
38
  const MAXMIND_LICENSE_KEY = process.env.License_key;
39
 
40
+ // ==================== GLOBALS ====================
41
  let ghosteryDB = null;
42
  let ddgTrackerRadar = { domains: {} };
43
  let tosdrCache = new Map();
44
  let geoReader = null;
45
  let useMaxMind = false;
46
+ const scanMutex = new Mutex();
47
+
48
+ // ==================== BROWSER POOL ====================
49
+ class BrowserPool {
50
+ constructor() {
51
+ this.browser = null;
52
+ this.launching = false;
53
+ this.waiters = [];
54
+ }
55
+
56
+ async getBrowser() {
57
+ if (this.browser) {
58
+ try {
59
+ await this.browser.version();
60
+ return this.browser;
61
+ } catch {
62
+ this.browser = null;
63
+ }
64
+ }
65
+
66
+ if (this.launching) {
67
+ await new Promise(r => {
68
+ this.waiters.push(r);
69
+ setTimeout(r, 8000); // max wait 8s
70
+ });
71
+ return this.getBrowser();
72
+ }
73
 
74
+ this.launching = true;
75
+ try {
76
+ this.browser = await chromium.launch({
77
+ headless: true,
78
+ args: [
79
+ '--no-sandbox',
80
+ '--disable-setuid-sandbox',
81
+ '--disable-dev-shm-usage',
82
+ '--disable-gpu',
83
+ '--single-process',
84
+ '--disable-extensions',
85
+ '--memory-pressure-off',
86
+ '--disable-blink-features=AutomationControlled'
87
+ ]
88
+ });
89
+ console.log('🚀 Browser launched');
90
+ this.waiters.forEach(r => r());
91
+ this.waiters = [];
92
+ } finally {
93
+ this.launching = false;
94
+ }
95
+
96
+ return this.browser;
97
+ }
98
+
99
+ async newTab() {
100
+ const browser = await this.getBrowser();
101
+ const context = await browser.newContext({
102
+ javaScriptEnabled: true,
103
+ ignoreHTTPSErrors: true,
104
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
105
+ });
106
+ const page = await context.newPage();
107
+ return { page, context };
108
+ }
109
+
110
+ async closeTab(context) {
111
+ await context.close().catch(() => {});
112
+ }
113
+
114
+ async shutdown() {
115
+ if (this.browser) {
116
+ await this.browser.close().catch(() => {});
117
+ this.browser = null;
118
+ }
119
+ }
120
+ }
121
+
122
+ const pool = new BrowserPool();
123
+
124
+ // ==================== INIT FUNCTIONS ====================
125
  async function initGhosteryDB() {
126
  try {
127
  const enginePath = path.join(__dirname, 'node_modules', '@ghostery', 'trackerdb', 'dist', 'trackerdb.engine');
 
135
  }
136
  }
137
 
 
138
  function loadDDGTrackerRadar() {
139
  try {
140
  const ddgPath = path.join(__dirname, 'data', 'ddg-tracker-radar.json');
 
147
  }
148
  }
149
 
 
150
  cron.schedule('0 0 * * *', async () => {
151
  await downloadDDGTrackerRadar();
152
  if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) {
 
154
  }
155
  });
156
 
 
157
  async function downloadDDGTrackerRadar() {
158
  try {
159
  const response = await axios.get('https://downloads.vivaldi.com/ddg/tds-v2-current.json', { timeout: 30000 });
 
177
  }
178
  }
179
 
 
180
  async function initGeoDatabase() {
 
181
  if (MAXMIND_ACCOUNT_ID && MAXMIND_LICENSE_KEY) {
182
  try {
183
  const dbPath = path.join(__dirname, 'data', 'geo', 'GeoLite2-City.mmdb');
184
+ if (!existsSync(dbPath)) await downloadMaxMindDatabase(dbPath);
 
 
185
  if (existsSync(dbPath)) {
186
  geoReader = await maxmind.open(dbPath);
187
  useMaxMind = true;
 
192
  console.warn('���️ MaxMind initialization failed, falling back to geoip-lite:', e.message);
193
  }
194
  }
 
195
  console.log('✅ Using geoip-lite for Geo mapping');
196
  }
197
 
 
198
  async function downloadMaxMindDatabase(dbPath) {
199
  const url = `https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz`;
200
  const dir = path.dirname(dbPath);
201
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
 
202
  console.log('📥 Downloading MaxMind GeoLite2-City...');
203
  const response = await axios.get(url, { responseType: 'stream', timeout: 120000 });
204
  const tarPath = path.join(dir, 'GeoLite2-City.tar.gz');
 
208
  writer.on('finish', resolve);
209
  writer.on('error', reject);
210
  });
 
 
 
211
  console.warn('⚠️ MaxMind DB downloaded but auto-extraction not implemented. Using geoip-lite instead.');
212
  }
213
 
214
+ // ==================== UTILS ====================
215
  function normalizeUrl(inputUrl) {
216
  let url = inputUrl.trim();
217
  if (!url.startsWith('http://') && !url.startsWith('https://')) url = 'https://' + url;
 
223
  return parsed.domain || hostname;
224
  }
225
 
 
226
  async function getGhosteryInfo(domain) {
227
  if (!ghosteryDB) return null;
228
  try {
 
242
  return null;
243
  }
244
 
 
245
  function getDDGInfo(domain) {
246
  const baseDomain = getBaseDomain(domain);
247
  return ddgTrackerRadar.domains[domain] || ddgTrackerRadar.domains[baseDomain] || null;
248
  }
249
 
 
250
  async function getTosdrGrade(url) {
251
  try {
252
  const hostname = new URL(url).hostname;
253
  if (tosdrCache.has(hostname)) return tosdrCache.get(hostname);
 
254
  const searchResponse = await axios.get(`https://api.tosdr.org/search/v5?query=${encodeURIComponent(hostname)}`, { timeout: 5000 });
255
  const services = searchResponse.data?.services || [];
256
+ if (services.length === 0) { tosdrCache.set(hostname, null); return null; }
 
 
 
 
257
  const serviceId = services[0].id;
258
  const detailResponse = await axios.get(`https://api.tosdr.org/service/v3?id=${serviceId}`, { timeout: 5000 });
259
  const service = detailResponse.data;
 
260
  const result = {
261
  grade: service.rating || 'N/A',
262
  name: service.name,
 
270
  }
271
  }
272
 
273
+ // ==================== BROWSER TOOLS (shared pool) ====================
274
+
275
  async function takeScreenshot(url) {
276
+ const { page, context } = await pool.newTab();
277
  try {
 
 
278
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
279
+ await page.waitForTimeout(800);
280
+ const buffer = await page.screenshot({ type: 'jpeg', quality: 80, fullPage: false });
281
  return `data:image/jpeg;base64,${buffer.toString('base64')}`;
282
  } catch (e) {
283
  console.error('Screenshot failed:', e.message);
284
  return null;
285
  } finally {
286
+ await pool.closeTab(context);
287
  }
288
  }
289
 
290
+ async function checkHiddenStorage(url) {
291
+ const { page, context } = await pool.newTab();
292
  try {
293
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
294
+ return await page.evaluate(() => ({
295
+ localStorage: Object.entries(localStorage).map(([k, v]) => ({ key: k, value: v })),
296
+ sessionStorage: Object.entries(sessionStorage).map(([k, v]) => ({ key: k, value: v })),
297
+ indexedDB: !!window.indexedDB
298
+ }));
 
 
 
 
 
 
299
  } catch (e) {
300
+ console.error('Hidden storage check failed:', e.message);
301
+ return null;
302
+ } finally {
303
+ await pool.closeTab(context);
304
  }
305
  }
306
 
307
+ async function analyzeCookieConsent(url) {
308
+ const { page, context } = await pool.newTab();
 
309
  try {
 
 
310
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
311
+ await page.waitForTimeout(1500); // انتظر الـ banner يظهر
312
+
313
+ return await page.evaluate(() => {
314
+ const bodyText = document.body.innerHTML.toLowerCase();
315
+ const allButtons = Array.from(document.querySelectorAll('button, a[role="button"], [class*="btn"]'))
316
+ .map(el => el.innerText?.toLowerCase().trim())
317
+ .filter(Boolean);
318
+
319
+ const hasAccept = allButtons.some(t =>
320
+ t.includes('accept') || t.includes('agree') || t.includes('allow') ||
321
+ t.includes('ok') || t.includes('got it') || t.includes('i agree')
322
+ );
323
+ const hasReject = allButtons.some(t =>
324
+ t.includes('reject') || t.includes('decline') || t.includes('refuse') ||
325
+ t.includes('no thanks') || t.includes('deny') || t.includes('opt out')
326
+ );
327
+ const hasBanner = !!(bodyText.match(/cookie|gdpr|consent|ccpa/));
328
+
329
+ const checkboxes = Array.from(document.querySelectorAll('input[type="checkbox"]'));
330
+ const preChecked = checkboxes.filter(el => el.checked).length;
331
+ const totalChecks = checkboxes.length;
332
+
333
+ // detect dark patterns
334
+ const darkPatterns = [];
335
+ if (hasAccept && !hasReject) darkPatterns.push('no_reject_option');
336
+ if (preChecked > 0) darkPatterns.push('pre_checked_boxes');
337
+
338
+ const hasCustomize = allButtons.some(t =>
339
+ t.includes('customize') || t.includes('settings') ||
340
+ t.includes('manage') || t.includes('preferences')
341
+ );
342
+
343
  return {
344
+ has_banner: hasBanner,
345
+ has_accept: hasAccept,
346
+ has_reject: hasReject,
347
+ has_customize: hasCustomize,
348
+ pre_checked_boxes: preChecked,
349
+ total_checkboxes: totalChecks,
350
+ dark_patterns: darkPatterns,
351
+ gdpr_compliant: hasAccept && hasReject,
352
+ dark_pattern_detected: darkPatterns.length > 0
353
  };
354
  });
 
355
  } catch (e) {
356
+ console.error('Cookie consent analysis failed:', e.message);
357
+ return null;
358
+ } finally {
359
+ await pool.closeTab(context);
360
+ }
361
+ }
362
+
363
+ async function collectFingerprintRisks(url) {
364
+ const { page, context } = await pool.newTab();
365
+ try {
366
+ // intercept scripts to detect fingerprint APIs
367
+ const fingerprintAPIs = {
368
+ canvas: false,
369
+ webgl: false,
370
+ audio: false,
371
+ fonts: false,
372
+ webrtc: false,
373
+ battery: false,
374
+ sensors: false,
375
+ mediaDevices: false
376
+ };
377
+
378
+ await page.addInitScript(() => {
379
+ // نتعقب استخدام الـ APIs
380
+ const orig = HTMLCanvasElement.prototype.toDataURL;
381
+ HTMLCanvasElement.prototype.toDataURL = function(...args) {
382
+ window.__fp_canvas = true;
383
+ return orig.apply(this, args);
384
+ };
385
+ const origGetContext = HTMLCanvasElement.prototype.getContext;
386
+ HTMLCanvasElement.prototype.getContext = function(type, ...args) {
387
+ if (type === 'webgl' || type === 'webgl2') window.__fp_webgl = true;
388
+ return origGetContext.apply(this, args);
389
+ };
390
+ const OrigAudio = window.AudioContext || window.webkitAudioContext;
391
+ if (OrigAudio) {
392
+ const OrigClass = OrigAudio;
393
+ window.AudioContext = window.webkitAudioContext = function(...args) {
394
+ window.__fp_audio = true;
395
+ return new OrigClass(...args);
396
+ };
397
+ }
398
+ const origGetBattery = navigator.getBattery;
399
+ if (origGetBattery) {
400
+ navigator.getBattery = function() {
401
+ window.__fp_battery = true;
402
+ return origGetBattery.call(navigator);
403
+ };
404
+ }
405
+ const origEnumerate = navigator.mediaDevices?.enumerateDevices;
406
+ if (origEnumerate) {
407
+ navigator.mediaDevices.enumerateDevices = function() {
408
+ window.__fp_media = true;
409
+ return origEnumerate.call(navigator.mediaDevices);
410
+ };
411
+ }
412
+ });
413
+
414
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15000 });
415
+ await page.waitForTimeout(2000); // انتظر scripts تشتغل
416
+
417
+ const result = await page.evaluate(() => ({
418
+ canvas: !!window.__fp_canvas,
419
+ webgl: !!window.__fp_webgl,
420
+ audio: !!window.__fp_audio,
421
+ battery: !!window.__fp_battery,
422
+ mediaDevices: !!window.__fp_media,
423
+ // APIs موجودة في البراوزر
424
+ webrtc_available: !!(window.RTCPeerConnection),
425
+ fonts_api: !!document.fonts,
426
+ device_memory: navigator.deviceMemory || null,
427
+ hardware_concurrency: navigator.hardwareConcurrency || null,
428
+ screen: {
429
+ width: screen.width,
430
+ height: screen.height,
431
+ depth: screen.colorDepth
432
+ },
433
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
434
+ languages: navigator.languages ? [...navigator.languages] : []
435
+ }));
436
+
437
+ // حساب fingerprint risk score
438
+ const risks = [];
439
+ if (result.canvas) risks.push('canvas_fingerprinting');
440
+ if (result.webgl) risks.push('webgl_fingerprinting');
441
+ if (result.audio) risks.push('audio_fingerprinting');
442
+ if (result.battery) risks.push('battery_status_api');
443
+ if (result.mediaDevices) risks.push('media_device_enumeration');
444
+ if (result.webrtc_available) risks.push('webrtc_ip_leak_risk');
445
+
446
+ return {
447
+ ...result,
448
+ risks_detected: risks,
449
+ risk_level: risks.length === 0 ? 'low' : risks.length <= 2 ? 'medium' : 'high',
450
+ risk_count: risks.length
451
+ };
452
+ } catch (e) {
453
+ console.error('Fingerprint risk collection failed:', e.message);
454
  return null;
455
  } finally {
456
+ await pool.closeTab(context);
457
+ }
458
+ }
459
+
460
+ // ==================== NON-BROWSER TOOLS ====================
461
+
462
+ async function trackRedirectChain(url) {
463
+ const TRACKING_PARAMS = [
464
+ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
465
+ 'fbclid', 'gclid', 'mc_eid', 'ref', 'affiliate',
466
+ 'clickid', 'adid', 'msclkid', 'twclid', '_ga', 'igshid'
467
+ ];
468
+
469
+ const chain = [];
470
+ let current = url;
471
+
472
+ for (let i = 0; i < 12; i++) {
473
+ try {
474
+ const response = await axios.get(current, {
475
+ maxRedirects: 0,
476
+ validateStatus: s => s < 500,
477
+ timeout: 5000,
478
+ headers: {
479
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
480
+ }
481
+ });
482
+
483
+ let foundParams = [];
484
+ try {
485
+ const urlObj = new URL(current);
486
+ foundParams = TRACKING_PARAMS.filter(p => urlObj.searchParams.has(p));
487
+ } catch {}
488
+
489
+ chain.push({
490
+ url: current,
491
+ status: response.status,
492
+ tracking_params: foundParams,
493
+ is_tracker: foundParams.length > 0
494
+ });
495
+
496
+ const location = response.headers.location;
497
+ if (!location) break;
498
+
499
+ current = location.startsWith('http')
500
+ ? location
501
+ : new URL(location, current).toString();
502
+
503
+ } catch (e) {
504
+ break;
505
+ }
506
+ }
507
+
508
+ const allTrackingParams = [...new Set(chain.flatMap(c => c.tracking_params))];
509
+
510
+ return {
511
+ chain,
512
+ total_redirects: Math.max(0, chain.length - 1),
513
+ has_tracking: chain.some(c => c.tracking_params.length > 0),
514
+ tracking_params_found: allTrackingParams,
515
+ risk_level: allTrackingParams.length === 0 ? 'low'
516
+ : allTrackingParams.length <= 2 ? 'medium' : 'high'
517
+ };
518
+ }
519
+
520
+ async function performBlacklightScan(url) {
521
+ try {
522
+ const tmpDir = path.join(__dirname, 'bl-tmp');
523
+ if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true });
524
+
525
+ const options = {
526
+ inUrl: url,
527
+ blTests: [
528
+ 'cookies', 'third_party_trackers', 'fb_pixel_events',
529
+ 'canvas_fingerprinters', 'canvas_font_fingerprinters',
530
+ 'key_logging', 'session_recorders', 'google_analytics_events',
531
+ 'twitter_pixel', 'tiktok_pixel'
532
+ ],
533
+ numPages: 1,
534
+ defaultWaitUntil: 'domcontentloaded',
535
+ captureHar: true,
536
+ saveScreenshots: false,
537
+ headless: true,
538
+ defaultTimeout: 30000,
539
+ extraChromiumArgs: [
540
+ '--disable-blink-features=AutomationControlled',
541
+ '--no-sandbox',
542
+ '--disable-setuid-sandbox',
543
+ '--disable-dev-shm-usage',
544
+ '--disable-gpu',
545
+ '--single-process',
546
+ '--ignore-certificate-errors'
547
+ ]
548
+ };
549
+ return await collect(url, options);
550
+ } catch (e) {
551
+ console.error('Blacklight scan failed:', e.message);
552
+ return { hosts: {}, cookies: [], error: e.message };
553
  }
554
  }
555
 
 
556
  async function performSecurityCheck(url) {
557
  try {
558
  const https = await import('node:https');
559
  const { hostname } = new URL(url);
 
560
  const cert = await new Promise((resolve, reject) => {
561
  const req = https.request({ hostname, port: 443, method: 'HEAD', timeout: 5000 }, (res) => {
562
+ resolve(res.socket.getPeerCertificate());
 
563
  });
564
  req.on('error', reject);
565
  req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
566
  req.end();
567
  });
 
568
  const valid = cert && Object.keys(cert).length > 0;
569
  const issuer = cert.issuer?.O || cert.issuer?.CN || 'Unknown';
570
  const expires = cert.valid_to ? new Date(cert.valid_to) : null;
571
  const daysRemaining = expires ? Math.floor((expires - Date.now()) / (1000 * 60 * 60 * 24)) : 0;
 
572
  return {
573
  ssl: {
574
  valid,
 
576
  expires_in_days: daysRemaining,
577
  protocol: 'TLS',
578
  grade: valid ? (daysRemaining > 30 ? 'A' : 'B') : 'F'
579
+ }
 
580
  };
581
  } catch (e) {
582
  return { ssl: { valid: false, error: e.message } };
583
  }
584
  }
585
 
586
+ // ==================== SCORING ====================
587
+ function calculatePrivacyScore(blacklight, enrichedTrackers, tosdrGrade, security, cookieConsent, fingerprintRisks, redirectChain) {
588
  let score = 100;
589
+
590
+ // trackers
591
+ score -= Math.min(enrichedTrackers.length * 4, 30);
592
+
593
+ // fingerprinting from blacklight
594
+ if (blacklight?.canvasFingerprinters?.length) score -= 15;
595
+ if (blacklight?.canvasFontFingerprinters?.length) score -= 10;
596
+
597
+ // fingerprint risks detected live
598
+ if (fingerprintRisks?.risk_count > 0) score -= Math.min(fingerprintRisks.risk_count * 5, 20);
599
+
600
+ // cookies
601
  const thirdPartyCookies = blacklight?.cookies?.filter(c => c.thirdParty)?.length || 0;
602
  score -= Math.min(thirdPartyCookies * 5, 20);
603
+
604
+ // session & keylogging
605
  if (blacklight?.sessionRecorders?.length > 0) score -= 10;
606
+ if (blacklight?.keyLogging?.length > 0) score -= 15;
607
+
608
+ // SSL
609
  if (!security?.ssl?.valid) score -= 20;
610
+
611
+ // cookie consent dark patterns
612
+ if (cookieConsent?.dark_pattern_detected) score -= 10;
613
+ if (cookieConsent?.has_banner && !cookieConsent?.gdpr_compliant) score -= 5;
614
+
615
+ // redirect tracking
616
+ if (redirectChain?.has_tracking) score -= Math.min(redirectChain.tracking_params_found.length * 3, 10);
617
+
618
+ // ToS;DR
619
  if (tosdrGrade) {
620
  if (tosdrGrade.grade === 'A') score += 5;
621
  else if (tosdrGrade.grade === 'B') score += 2;
622
  else if (tosdrGrade.grade === 'D') score -= 5;
623
  else if (tosdrGrade.grade === 'E') score -= 10;
624
  }
625
+
626
  score = Math.max(0, Math.min(100, Math.round(score)));
627
+
628
  let grade;
629
  if (score >= 95) grade = 'A+';
630
  else if (score >= 85) grade = 'A';
 
632
  else if (score >= 65) grade = 'C';
633
  else if (score >= 55) grade = 'D';
634
  else grade = 'F';
635
+
636
  return { score, grade };
637
  }
638
 
639
+ // ==================== API ENDPOINTS ====================
640
+
641
  app.get('/api/scan', async (req, res) => {
642
  const url = normalizeUrl(req.query.url);
643
  if (!url) return res.status(400).json({ error: 'Invalid URL' });
644
+
645
+ // منع التزامن — طلب واحد في كل مرة
646
+ if (scanMutex.isLocked()) {
647
+ return res.status(429).json({
648
+ error: 'Scanner is busy, please try again in a moment.',
649
+ retry_after: 30
650
+ });
651
+ }
652
+
653
+ const release = await scanMutex.acquire();
654
  const startTime = Date.now();
655
+
656
  try {
657
+ // المتصفح الواحد — كل tool في tab منفصل (بالتوازي)
658
+ const [
659
+ screenshotResult,
660
+ hiddenStorageResult,
661
+ cookieConsentResult,
662
+ fingerprintResult,
663
+ // هذه لا تحتاج browser
664
+ redirectChainResult,
665
+ blacklightResult,
666
+ tosdrResult,
667
+ securityResult
668
+ ] = await Promise.allSettled([
669
  takeScreenshot(url),
670
+ checkHiddenStorage(url),
671
+ analyzeCookieConsent(url),
672
+ collectFingerprintRisks(url),
673
+ trackRedirectChain(url),
674
  performBlacklightScan(url),
675
  getTosdrGrade(url),
676
+ performSecurityCheck(url)
 
677
  ]);
678
+
679
+ const screenshotData = screenshotResult.status === 'fulfilled' ? screenshotResult.value : null;
680
+ const blacklightData = blacklightResult.status === 'fulfilled' ? blacklightResult.value : { hosts: {}, cookies: [] };
681
+ const tosdrData = tosdrResult.status === 'fulfilled' ? tosdrResult.value : null;
682
+ const securityData = securityResult.status === 'fulfilled' ? securityResult.value : { ssl: { valid: false } };
683
+ const hiddenData = hiddenStorageResult.status === 'fulfilled' ? hiddenStorageResult.value : null;
684
+ const cookieConsentData = cookieConsentResult.status === 'fulfilled' ? cookieConsentResult.value : null;
685
+ const fingerprintData = fingerprintResult.status === 'fulfilled' ? fingerprintResult.value : null;
686
+ const redirectData = redirectChainResult.status === 'fulfilled' ? redirectChainResult.value : null;
687
+
688
+ // ==================== TRACKERS ====================
689
  const thirdPartyDomains = [
690
  ...(blacklightData.hosts?.thirdParty || []),
691
  ...(blacklightData.hosts?.requests?.third_party || [])
692
  ];
693
  const uniqueDomains = [...new Set(thirdPartyDomains)];
 
 
694
  const enrichedTrackers = [];
695
  for (const domain of uniqueDomains) {
696
  try {
 
703
  prevalence: ddgInfo?.prevalence || 0
704
  });
705
  } catch (e) {
706
+ enrichedTrackers.push({ domain, owner: getBaseDomain(domain), category: 'unknown', prevalence: 0 });
 
 
 
 
 
707
  }
708
  }
709
+
710
+ // ==================== GEO MAPPING ====================
711
  const uniqueIps = [...new Set(
712
+ (blacklightData.hosts?.requests?.third_party || [])
713
+ .map(r => r.ip_addr).filter(Boolean)
714
  )];
715
  const geoDestinations = [];
716
  for (const ip of uniqueIps) {
 
720
  const mmGeo = geoReader.get(ip);
721
  if (mmGeo) {
722
  geoData = {
723
+ ip,
724
  country: mmGeo.country?.names?.en || 'Unknown',
725
  city: mmGeo.city?.names?.en || 'Unknown',
726
  latitude: mmGeo.location?.latitude,
 
732
  const geoLite = geoip.lookup(ip);
733
  if (geoLite) {
734
  geoData = {
735
+ ip,
736
  country: geoLite.country,
737
  city: geoLite.city,
738
+ latitude: geoLite.ll?.[0] || null,
739
+ longitude: geoLite.ll?.[1] || null
740
  };
741
  }
742
  }
743
  if (geoData) geoDestinations.push(geoData);
744
  } catch (e) {}
745
  }
746
+
747
+ // ==================== LEAKAGE ====================
748
  const leakageAlerts = [];
749
+ for (const r of (blacklightData.hosts?.requests?.third_party || [])) {
750
+ if ((r.method === 'POST' || r.method === 'PUT') && r.body) {
 
751
  try {
752
+ const detected = piiFilter.detect(r.body);
753
+ if (detected?.length > 0) {
754
  leakageAlerts.push({
755
  severity: 'high',
756
+ destination: r.url,
757
+ method: r.method,
758
  types: detected.map(p => p.type),
759
  message: 'Potential PII detected in request to third-party domain.'
760
  });
 
762
  } catch (e) {}
763
  }
764
  }
765
+
766
+ // ==================== SCORE ====================
767
+ const { score, grade } = calculatePrivacyScore(
768
+ blacklightData, enrichedTrackers, tosdrData,
769
+ securityData, cookieConsentData, fingerprintData, redirectData
770
+ );
771
+
772
+ // ==================== RESPONSE ====================
773
  const result = {
774
  success: true,
775
  url,
776
  final_url: blacklightData.uri_dest || url,
777
  scan_time_sec: (Date.now() - startTime) / 1000,
778
  privacy_score: { score, grade },
779
+
780
+ trackers: {
781
+ count: enrichedTrackers.length,
782
+ list: enrichedTrackers.slice(0, 20)
783
+ },
784
  cookies: {
785
  total: blacklightData.cookies?.length || 0,
786
  third_party: blacklightData.cookies?.filter(c => c.thirdParty)?.length || 0
787
  },
788
  fingerprinting: {
789
  canvas: !!(blacklightData.canvasFingerprinters?.length),
790
+ fonts: !!(blacklightData.canvasFontFingerprinters?.length),
791
+ live_risks: fingerprintData
792
  },
793
  session_recording: !!(blacklightData.sessionRecorders?.length),
794
  key_logging: !!(blacklightData.keyLogging?.length),
 
797
  sessionStorage: hiddenData.sessionStorage?.length || 0,
798
  indexedDB: hiddenData.indexedDB
799
  } : null,
800
+ cookie_consent: cookieConsentData,
801
+ redirect_chain: redirectData,
802
  security: securityData,
803
  tosdr: tosdrData,
804
+ geo_mapping: { data_destinations: geoDestinations },
805
+ leakage_detection: { alerts: leakageAlerts },
 
 
 
 
806
  screenshot: screenshotData,
807
  raw: blacklightData
808
  };
809
+
810
  res.json(result);
811
+
812
  } catch (e) {
813
  console.error('Scan error:', e);
814
  res.status(500).json({ success: false, error: e.message });
815
+ } finally {
816
+ release();
817
  }
818
  });
819
 
820
  app.get('/health', (req, res) => res.json({ status: 'ok' }));
821
  app.use(express.static('public'));
822
 
823
+ // ==================== STARTUP ====================
824
  (async () => {
825
+ // تأكد أن المجلدات موجودة
826
+ const dirs = [
827
+ path.join(__dirname, 'data'),
828
+ path.join(__dirname, 'bl-tmp'),
829
+ path.join(__dirname, 'public')
830
+ ];
831
+ for (const dir of dirs) {
832
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
833
+ }
834
+
835
  await initGhosteryDB();
836
  loadDDGTrackerRadar();
837
  await initGeoDatabase();
838
+
839
  if (Object.keys(ddgTrackerRadar.domains).length === 0) {
840
  await downloadDDGTrackerRadar();
841
  }
842
+
843
  app.listen(PORT, '0.0.0.0', () => {
844
  console.log(`🚀 Private Eye with Geo & Leak Detection running on ${PORT}`);
845
  });