@fanboynz/network-scanner 2.0.58 → 2.0.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/nwss.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  // puppeteer for browser automation, fs for file system operations, psl for domain parsing.
4
4
  // const pLimit = require('p-limit'); // Will be dynamically imported
5
- const puppeteer = require('puppeteer');
5
+ const usePuppeteerCore = process.argv.includes('--use-puppeteer-core');
6
+ const puppeteer = usePuppeteerCore ? require('puppeteer-core') : require('puppeteer');
6
7
  const fs = require('fs');
7
8
  const os = require('os');
8
9
  const psl = require('psl');
@@ -31,7 +32,7 @@ const { shouldIgnoreSimilarDomain, calculateSimilarity } = require('./lib/ignore
31
32
  // Graceful exit
32
33
  const { handleBrowserExit, cleanupChromeTempFiles } = require('./lib/browserexit');
33
34
  // Whois & Dig
34
- const { createNetToolsHandler, createEnhancedDryRunCallback, validateWhoisAvailability, validateDigAvailability } = require('./lib/nettools');
35
+ const { createNetToolsHandler, createEnhancedDryRunCallback, validateWhoisAvailability, validateDigAvailability, enableDiskCache, getDnsCacheStats } = require('./lib/nettools');
35
36
  // File compare
36
37
  const { loadComparisonRules, filterUniqueRules } = require('./lib/compare');
37
38
  // CDP functionality
@@ -42,6 +43,8 @@ const { processResults } = require('./lib/post-processing');
42
43
  const { colorize, colors, messageColors, tags, formatLogMessage } = require('./lib/colorize');
43
44
  // Enhanced mouse interaction and page simulation
44
45
  const { performPageInteraction, createInteractionConfig, performContentClicks, humanLikeMouseMove } = require('./lib/interaction');
46
+ // Optional ghost-cursor support for advanced Bezier-based mouse movements
47
+ const { isGhostCursorAvailable, createGhostCursor, ghostMove, ghostClick, ghostRandomMove, resolveGhostCursorConfig } = require('./lib/ghost-cursor');
45
48
  // Domain detection cache for performance optimization
46
49
  const { createGlobalHelpers, getTotalDomainsSkipped, getDetectedDomainsCount } = require('./lib/domain-cache');
47
50
  const { createSmartCache } = require('./lib/smart-cache'); // Smart cache system
@@ -118,7 +121,7 @@ const REALTIME_CLEANUP_THRESHOLD = 8; // Default pages to keep for realtime clea
118
121
  */
119
122
  function detectPuppeteerVersion() {
120
123
  try {
121
- const puppeteer = require('puppeteer');
124
+ const puppeteer = usePuppeteerCore ? require('puppeteer-core') : require('puppeteer');
122
125
  let versionString = null;
123
126
 
124
127
  // Try multiple methods to get version
@@ -204,7 +207,15 @@ const localhostIndex = args.findIndex(arg => arg.startsWith('--localhost'));
204
207
  if (localhostIndex !== -1) {
205
208
  localhostIP = args[localhostIndex].includes('=') ? args[localhostIndex].split('=')[1] : '127.0.0.1';
206
209
  }
210
+ const keepBrowserOpen = args.includes('--keep-open');
211
+ const loadExtensionPaths = [];
212
+ args.forEach((arg, idx) => {
213
+ if (arg === '--load-extension' && args[idx + 1] && !args[idx + 1].startsWith('--')) {
214
+ loadExtensionPaths.push(path.resolve(args[idx + 1]));
215
+ }
216
+ });
207
217
  const disableInteract = args.includes('--no-interact');
218
+ const globalGhostCursor = args.includes('--ghost-cursor');
208
219
  const plainOutput = args.includes('--plain');
209
220
  const enableCDP = args.includes('--cdp');
210
221
  const dnsmasqMode = args.includes('--dnsmasq');
@@ -224,6 +235,8 @@ let cleanRules = args.includes('--clean-rules');
224
235
  const clearCache = args.includes('--clear-cache');
225
236
  const ignoreCache = args.includes('--ignore-cache');
226
237
  const cacheRequests = args.includes('--cache-requests');
238
+ const dnsCacheMode = args.includes('--dns-cache');
239
+ if (dnsCacheMode) enableDiskCache();
227
240
 
228
241
  let validateRulesFile = null;
229
242
  const validateRulesIndex = args.findIndex(arg => arg === '--validate-rules');
@@ -494,22 +507,38 @@ if (validateRules || validateRulesFile) {
494
507
  }
495
508
  }
496
509
 
497
- // Parse --block-ads argument for request-level ad blocking
510
+ // Parse --block-ads argument for request-level ad blocking (supports comma-separated lists)
498
511
  const blockAdsIndex = args.findIndex(arg => arg.startsWith('--block-ads'));
499
512
  if (blockAdsIndex !== -1) {
500
- const rulesFile = args[blockAdsIndex].includes('=')
501
- ? args[blockAdsIndex].split('=')[1]
513
+ const rulesArg = args[blockAdsIndex].includes('=')
514
+ ? args[blockAdsIndex].split('=')[1]
502
515
  : args[blockAdsIndex + 1];
503
-
504
- if (!rulesFile || !fs.existsSync(rulesFile)) {
505
- console.log(`Error: Adblock rules file not found: ${rulesFile || '(not specified)'}`);
516
+
517
+ if (!rulesArg) {
518
+ console.log('Error: No adblock rules file specified');
506
519
  process.exit(1);
507
520
  }
508
-
521
+
522
+ const rulesFiles = rulesArg.split(',').map(f => f.trim()).filter(f => f);
523
+ for (const file of rulesFiles) {
524
+ if (!fs.existsSync(file)) {
525
+ console.log(`Error: Adblock rules file not found: ${file}`);
526
+ process.exit(1);
527
+ }
528
+ }
529
+
530
+ // Concatenate multiple lists into a single temp file for the parser
531
+ let rulesFile = rulesFiles[0];
532
+ if (rulesFiles.length > 1) {
533
+ rulesFile = path.join(os.tmpdir(), `nwss-adblock-combined-${Date.now()}.txt`);
534
+ const combined = rulesFiles.map(f => fs.readFileSync(f, 'utf-8')).join('\n');
535
+ fs.writeFileSync(rulesFile, combined);
536
+ }
537
+
509
538
  adblockEnabled = true;
510
539
  adblockMatcher = parseAdblockRules(rulesFile, { enableLogging: forceDebug });
511
540
  const stats = adblockMatcher.getStats();
512
- if (!silentMode) console.log(messageColors.success(`Adblock enabled: Loaded ${stats.total} blocking rules from ${rulesFile}`));
541
+ if (!silentMode) console.log(messageColors.success(`Adblock enabled: Loaded ${stats.total} blocking rules from ${rulesFiles.length} list${rulesFiles.length > 1 ? 's' : ''}`));
513
542
  }
514
543
 
515
544
  if (args.includes('--help') || args.includes('-h')) {
@@ -546,8 +575,12 @@ General Options:
546
575
  --compress-logs Compress log files with gzip (requires --dumpurls)
547
576
  --sub-domains Output full subdomains instead of collapsing to root
548
577
  --no-interact Disable page interactions globally
578
+ --ghost-cursor Use ghost-cursor Bezier mouse movements (requires: npm i ghost-cursor)
549
579
  --custom-json <file> Use a custom config JSON file instead of config.json
550
580
  --headful Launch browser with GUI (not headless)
581
+ --keep-open Keep browser open after scan completes (use with --headful)
582
+ --use-puppeteer-core Use puppeteer-core with system Chrome instead of bundled Chromium
583
+ --load-extension <path> Load unpacked Chrome extension from directory
551
584
  --cdp Enable Chrome DevTools Protocol logging (now per-page if enabled)
552
585
  --remove-dupes Remove duplicate domains from output (only with -o)
553
586
  --eval-on-doc Globally enable evaluateOnNewDocument() for Fetch/XHR interception
@@ -559,6 +592,7 @@ General Options:
559
592
 
560
593
  Validation Options:
561
594
  --cache-requests Cache HTTP requests to avoid re-requesting same URLs within scan
595
+ --dns-cache Persist dig/whois results to disk between runs (3hr/4hr TTL)
562
596
  --validate-config Validate config.json file and exit
563
597
  --validate-rules [file] Validate rule file format (uses --output/--compare files if no file specified)
564
598
  --clean-rules [file] Clean rule files by removing invalid lines and optionally duplicates (uses --output/--compare files if no file specified)
@@ -575,6 +609,7 @@ Global config.json options:
575
609
  ignore_similar_ignored_domains: true/false Ignore domains similar to ignoreDomains list (default: true)
576
610
  max_concurrent_sites: 8 Maximum concurrent site processing (1-50, default: 8)
577
611
  resource_cleanup_interval: 80 Browser restart interval in URLs processed (1-1000, default: 80)
612
+ disable_ad_tagging: true/false Disable Chrome AdTagging to prevent ad frame throttling (default: true)
578
613
 
579
614
  Per-site config.json options:
580
615
  url: "site" or ["site1", "site2"] Single URL or list of URLs
@@ -658,6 +693,11 @@ Advanced Options:
658
693
  interact_scrolling: true/false Enable scrolling simulation (default: true)
659
694
  interact_clicks: true/false Enable element clicking simulation (default: false)
660
695
  interact_typing: true/false Enable typing simulation (default: false)
696
+ cursor_mode: "ghost" Use ghost-cursor Bezier mouse (requires: npm i ghost-cursor)
697
+ ghost_cursor_speed: <number> Ghost-cursor speed multiplier (default: auto)
698
+ ghost_cursor_hesitate: <milliseconds> Delay before ghost-cursor clicks (default: 50)
699
+ ghost_cursor_overshoot: <pixels> Max ghost-cursor overshoot distance (default: auto)
700
+ ghost_cursor_duration: <milliseconds> Ghost-cursor interaction duration (default: interact_duration or 2000)
661
701
  whois: ["term1", "term2"] Check whois data for ALL specified terms (AND logic)
662
702
  whois-or: ["term1", "term2"] Check whois data for ANY specified term (OR logic)
663
703
  whois_server_mode: "random" or "cycle" Server selection mode: random (default) or cycle through list
@@ -735,7 +775,8 @@ const {
735
775
  whois_server_mode = 'random',
736
776
  ignore_similar = true,
737
777
  ignore_similar_threshold = 80,
738
- ignore_similar_ignored_domains = true,
778
+ ignore_similar_ignored_domains = true,
779
+ disable_ad_tagging = true,
739
780
  max_concurrent_sites = 6,
740
781
  resource_cleanup_interval = 80,
741
782
  comments: globalComments,
@@ -1430,6 +1471,10 @@ function setupFrameHandling(page, forceDebug) {
1430
1471
  break;
1431
1472
  }
1432
1473
  }
1474
+ if (usePuppeteerCore && !executablePath) {
1475
+ console.error(formatLogMessage('error', '--use-puppeteer-core requires a system Chrome installation. No Chrome found in standard paths.'));
1476
+ process.exit(1);
1477
+ }
1433
1478
  const browser = await puppeteer.launch({
1434
1479
  // Use system Chrome if available to avoid downloads
1435
1480
  executablePath: executablePath,
@@ -1443,7 +1488,7 @@ function setupFrameHandling(page, forceDebug) {
1443
1488
  '--disable-blink-features=AutomationControlled',
1444
1489
  '--no-first-run',
1445
1490
  '--disable-default-apps',
1446
- '--disable-component-extensions-with-background-pages',
1491
+ ...(keepBrowserOpen ? [] : ['--disable-component-extensions-with-background-pages']),
1447
1492
  // HIGH IMPACT: Normal Chrome behavior simulation
1448
1493
  '--password-store=basic',
1449
1494
  '--use-mock-keychain',
@@ -1457,30 +1502,29 @@ function setupFrameHandling(page, forceDebug) {
1457
1502
  '--disable-background-downloads',
1458
1503
  // DISK I/O REDUCTION: Eliminate unnecessary Chrome disk writes
1459
1504
  '--disable-breakpad', // No crash dump files
1460
- '--disable-component-update', // No component update downloads
1505
+ ...(keepBrowserOpen ? [] : ['--disable-component-update']), // No component update downloads
1461
1506
  '--disable-logging', // No Chrome internal log files
1462
1507
  '--log-level=3', // Fatal errors only (suppresses verbose disk logging)
1463
1508
  '--no-service-autorun', // No background service disk activity
1464
1509
  '--disable-domain-reliability', // No reliability monitor disk writes
1465
- // PERFORMANCE: Enhanced Puppeteer 23.x optimizations
1466
- '--disable-features=AudioServiceOutOfProcess,VizDisplayCompositor',
1467
- '--disable-features=TranslateUI,BlinkGenPropertyTrees,Translate',
1468
- '--disable-features=BackForwardCache,AcceptCHFrame',
1510
+ // PERFORMANCE: Disable non-essential Chrome features in a single flag
1511
+ // IMPORTANT: Chrome only reads the LAST --disable-features flag, so combine all into one
1512
+ `--disable-features=AudioServiceOutOfProcess,VizDisplayCompositor,TranslateUI,BlinkGenPropertyTrees,Translate,BackForwardCache,AcceptCHFrame,SafeBrowsing,HttpsFirstBalancedModeAutoEnable,site-per-process,PaintHolding${disable_ad_tagging ? ',AdTagging' : ''}`,
1469
1513
  '--disable-ipc-flooding-protection',
1470
1514
  '--aggressive-cache-discard',
1471
1515
  '--memory-pressure-off',
1472
1516
  '--max_old_space_size=2048', // V8 heap limit
1473
1517
  '--disable-prompt-on-repost', // Fixes form popup on page reload
1474
- '--disable-background-networking',
1518
+ ...(keepBrowserOpen ? [] : ['--disable-background-networking']),
1475
1519
  '--no-sandbox',
1476
1520
  '--disable-setuid-sandbox',
1477
- '--disable-features=SafeBrowsing',
1478
1521
  '--disable-dev-shm-usage',
1479
- '--disable-sync',
1522
+ ...(keepBrowserOpen ? [] : ['--disable-sync']),
1480
1523
  '--mute-audio',
1481
1524
  '--disable-translate',
1482
1525
  '--window-size=1920,1080',
1483
- '--disable-extensions',
1526
+ ...(keepBrowserOpen ? [] : ['--disable-extensions', '--disable-component-update']),
1527
+ ...(loadExtensionPaths.length ? [`--load-extension=${loadExtensionPaths.join(',')}`, '--enable-extensions'] : []),
1484
1528
  '--no-default-browser-check',
1485
1529
  '--safebrowsing-disable-auto-update',
1486
1530
  '--ignore-ssl-errors',
@@ -1489,18 +1533,15 @@ function setupFrameHandling(page, forceDebug) {
1489
1533
  '--ignore-certificate-errors-ca-list',
1490
1534
  '--disable-web-security',
1491
1535
  '--allow-running-insecure-content',
1492
- '--disable-features=HttpsFirstBalancedModeAutoEnable',
1493
1536
  // Puppeteer 23.x: Enhanced performance and stability args
1494
1537
  '--disable-renderer-backgrounding',
1495
1538
  '--disable-backgrounding-occluded-windows',
1496
1539
  '--disable-background-timer-throttling',
1497
- '--disable-features=site-per-process', // Better for single-site scanning
1498
1540
  '--no-zygote', // Better process isolation
1499
1541
  // PERFORMANCE: Process and memory reduction for high concurrency
1500
1542
  '--renderer-process-limit=10', // Cap renderer processes (default: unlimited)
1501
1543
  '--disable-accelerated-2d-canvas', // Software canvas only (we spoof it anyway)
1502
1544
  '--disable-hang-monitor', // Remove per-renderer hang check overhead
1503
- '--disable-features=PaintHolding', // Don't hold frames in renderer memory
1504
1545
  '--js-flags=--max-old-space-size=512', // Cap V8 heap per renderer to 512MB
1505
1546
  ...extraArgs,
1506
1547
  ],
@@ -3420,53 +3461,115 @@ function setupFrameHandling(page, forceDebug) {
3420
3461
  }
3421
3462
  }
3422
3463
 
3423
- if (interactEnabled && !disableInteract) {
3424
- if (forceDebug) console.log(formatLogMessage('debug', `interaction simulation enabled for ${currentUrl}`));
3425
-
3426
- // Mark page as processing during interactions
3427
- updatePageUsage(page, true);
3428
- // Use enhanced interaction module with hard abort timeout
3429
- const INTERACTION_HARD_TIMEOUT = 15000;
3430
- try {
3431
- await Promise.race([
3432
- performPageInteraction(page, currentUrl, interactionConfig, forceDebug),
3433
- new Promise((_, reject) => setTimeout(() => reject(new Error('interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
3434
- ]);
3435
- } catch (interactTimeoutErr) {
3436
- if (forceDebug) console.log(formatLogMessage('debug', `[interaction] Aborted after ${INTERACTION_HARD_TIMEOUT}ms: ${interactTimeoutErr.message}`));
3437
- }
3438
- }
3439
-
3440
3464
  const delayMs = DEFAULT_DELAY;
3441
-
3465
+
3442
3466
  // Optimized delays for Puppeteer 23.x performance
3443
3467
  const isFastSite = timeout <= TIMEOUTS.FAST_SITE_THRESHOLD;
3444
3468
  const networkIdleTime = TIMEOUTS.NETWORK_IDLE; // Balanced: 2s for reliable network detection
3445
3469
  const networkIdleTimeout = Math.min(timeout / 2, TIMEOUTS.NETWORK_IDLE_MAX); // Balanced: 10s timeout
3446
3470
  const actualDelay = Math.min(delayMs, TIMEOUTS.NETWORK_IDLE); // Balanced: 2s delay for stability
3447
-
3448
- // FIX: Check page state before waiting for network idle
3449
- if (page && !page.isClosed()) {
3450
- try {
3451
- await page.waitForNetworkIdle({
3452
- idleTime: networkIdleTime,
3453
- timeout: networkIdleTimeout
3454
- });
3455
- } catch (networkIdleErr) {
3456
- // Page closed or network idle timeout - continue anyway
3457
- if (forceDebug) console.log(formatLogMessage('debug', `Network idle wait failed: ${networkIdleErr.message}`));
3471
+
3472
+ // Build delay promise (networkIdle + delay + optional flowProxy delay)
3473
+ const delayPromise = (async () => {
3474
+ if (page && !page.isClosed()) {
3475
+ try {
3476
+ await page.waitForNetworkIdle({
3477
+ idleTime: networkIdleTime,
3478
+ timeout: networkIdleTimeout
3479
+ });
3480
+ } catch (networkIdleErr) {
3481
+ if (forceDebug) console.log(formatLogMessage('debug', `Network idle wait failed: ${networkIdleErr.message}`));
3482
+ }
3458
3483
  }
3459
- }
3484
+ await fastTimeout(actualDelay);
3485
+ if (flowproxyDetection) {
3486
+ const additionalDelay = Math.min(siteConfig.flowproxy_additional_delay || 3000, 3000);
3487
+ if (forceDebug) console.log(formatLogMessage('debug', `Applying flowProxy additional delay: ${additionalDelay}ms`));
3488
+ await fastTimeout(additionalDelay);
3489
+ }
3490
+ })();
3491
+
3492
+ // Build interaction promise — runs concurrently with delay
3493
+ const interactPromise = (async () => {
3494
+ if (!(interactEnabled && !disableInteract)) return;
3495
+ if (forceDebug) console.log(formatLogMessage('debug', `interaction simulation enabled for ${currentUrl}`));
3460
3496
 
3461
- // Use fast timeout helper for Puppeteer 23.x compatibility with better performance
3462
- await fastTimeout(actualDelay);
3497
+ // Mark page as processing during interactions
3498
+ updatePageUsage(page, true);
3499
+ const INTERACTION_HARD_TIMEOUT = 15000;
3463
3500
 
3464
- // Apply additional delay for flowProxy if detected
3465
- if (flowproxyDetection) {
3466
- const additionalDelay = Math.min(siteConfig.flowproxy_additional_delay || 3000, 3000);
3467
- if (forceDebug) console.log(formatLogMessage('debug', `Applying flowProxy additional delay: ${additionalDelay}ms`));
3468
- await fastTimeout(additionalDelay);
3469
- }
3501
+ // Check if ghost-cursor mode is enabled for this site
3502
+ const ghostConfig = resolveGhostCursorConfig(siteConfig, globalGhostCursor, forceDebug);
3503
+
3504
+ try {
3505
+ if (ghostConfig) {
3506
+ // Ghost-cursor mode: Bezier-based mouse movements
3507
+ if (forceDebug) console.log(formatLogMessage('debug', `[ghost-cursor] Using ghost-cursor for ${currentUrl}`));
3508
+ const cursor = createGhostCursor(page, { forceDebug });
3509
+ if (cursor) {
3510
+ await Promise.race([
3511
+ (async () => {
3512
+ const viewport = page.viewport() || { width: 1200, height: 800 };
3513
+ const ghostDuration = ghostConfig.duration || 2000;
3514
+ const ghostStart = Date.now();
3515
+ const ghostTimeLeft = () => ghostDuration - (Date.now() - ghostStart);
3516
+
3517
+ // Time-based Bezier mouse movements — runs for ghostDuration ms
3518
+ while (ghostTimeLeft() > 200) {
3519
+ const toX = Math.floor(Math.random() * (viewport.width - 100)) + 50;
3520
+ const toY = Math.floor(Math.random() * (viewport.height - 100)) + 50;
3521
+ await ghostMove(cursor, toX, toY, {
3522
+ moveSpeed: ghostConfig.moveSpeed,
3523
+ overshootThreshold: ghostConfig.overshootThreshold,
3524
+ forceDebug
3525
+ });
3526
+ if (ghostTimeLeft() > 100) {
3527
+ await new Promise(r => setTimeout(r, 25 + Math.random() * 75));
3528
+ }
3529
+ }
3530
+ if (ghostTimeLeft() > 100 && Math.random() < 0.3) {
3531
+ await ghostRandomMove(cursor, { forceDebug });
3532
+ }
3533
+ if (interactionConfig.includeElementClicks && ghostTimeLeft() > 100) {
3534
+ const clickX = Math.floor(viewport.width * 0.2 + Math.random() * viewport.width * 0.6);
3535
+ const clickY = Math.floor(viewport.height * 0.2 + Math.random() * viewport.height * 0.6);
3536
+ await ghostClick(cursor, { x: clickX, y: clickY }, {
3537
+ hesitate: ghostConfig.hesitate,
3538
+ forceDebug
3539
+ });
3540
+ }
3541
+ if (interactionConfig.includeScrolling) {
3542
+ await performPageInteraction(page, currentUrl, {
3543
+ ...interactionConfig,
3544
+ mouseMovements: 0,
3545
+ includeElementClicks: false,
3546
+ includeTyping: false
3547
+ }, forceDebug);
3548
+ }
3549
+ })(),
3550
+ new Promise((_, reject) => setTimeout(() => reject(new Error('ghost-cursor interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
3551
+ ]);
3552
+ } else {
3553
+ if (forceDebug) console.log(formatLogMessage('debug', '[ghost-cursor] Falling back to built-in mouse'));
3554
+ await Promise.race([
3555
+ performPageInteraction(page, currentUrl, interactionConfig, forceDebug),
3556
+ new Promise((_, reject) => setTimeout(() => reject(new Error('interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
3557
+ ]);
3558
+ }
3559
+ } else {
3560
+ // Standard built-in mouse interaction
3561
+ await Promise.race([
3562
+ performPageInteraction(page, currentUrl, interactionConfig, forceDebug),
3563
+ new Promise((_, reject) => setTimeout(() => reject(new Error('interaction hard timeout')), INTERACTION_HARD_TIMEOUT))
3564
+ ]);
3565
+ }
3566
+ } catch (interactTimeoutErr) {
3567
+ if (forceDebug) console.log(formatLogMessage('debug', `[interaction] Aborted after ${INTERACTION_HARD_TIMEOUT}ms: ${interactTimeoutErr.message}`));
3568
+ }
3569
+ })();
3570
+
3571
+ // Run delay and mouse interaction concurrently — mouse moves while page settles
3572
+ await Promise.all([delayPromise, interactPromise]);
3470
3573
 
3471
3574
  // Use fast timeout helper for consistent Puppeteer 23.x compatibility
3472
3575
 
@@ -3898,12 +4001,14 @@ function setupFrameHandling(page, forceDebug) {
3898
4001
  }
3899
4002
  }
3900
4003
 
3901
- try {
3902
- untrackPage(page);
3903
- await page.close();
3904
- if (forceDebug) console.log(formatLogMessage('debug', `Page closed for ${currentUrl}`));
3905
- } catch (pageCloseErr) {
3906
- if (forceDebug) console.log(formatLogMessage('debug', `Failed to close page for ${currentUrl}: ${pageCloseErr.message}`));
4004
+ if (!keepBrowserOpen) {
4005
+ try {
4006
+ untrackPage(page);
4007
+ await page.close();
4008
+ if (forceDebug) console.log(formatLogMessage('debug', `Page closed for ${currentUrl}`));
4009
+ } catch (pageCloseErr) {
4010
+ if (forceDebug) console.log(formatLogMessage('debug', `Failed to close page for ${currentUrl}: ${pageCloseErr.message}`));
4011
+ }
3907
4012
  }
3908
4013
  }
3909
4014
  }
@@ -4517,6 +4622,24 @@ function setupFrameHandling(page, forceDebug) {
4517
4622
  wgDisconnectAll(forceDebug);
4518
4623
  ovpnDisconnectAll(forceDebug);
4519
4624
 
4625
+ // Keep browser open if --keep-open flag is set (useful with --headful for inspection)
4626
+ if (keepBrowserOpen && !launchHeadless) {
4627
+ console.log(messageColors.info('Browser kept open.') + ' Close the browser window or press Ctrl+C to exit.');
4628
+ const cleanup = async () => {
4629
+ try {
4630
+ if (browser.isConnected()) await browser.close();
4631
+ } catch {}
4632
+ process.exit(0);
4633
+ };
4634
+ process.on('SIGINT', cleanup);
4635
+ process.on('SIGTERM', cleanup);
4636
+ await new Promise((resolve) => {
4637
+ browser.on('disconnected', resolve);
4638
+ });
4639
+ process.removeListener('SIGINT', cleanup);
4640
+ process.removeListener('SIGTERM', cleanup);
4641
+ }
4642
+
4520
4643
  // Perform comprehensive final cleanup using enhanced browserexit module
4521
4644
  if (forceDebug) console.log(formatLogMessage('debug', `Starting comprehensive browser cleanup...`));
4522
4645
 
@@ -4541,7 +4664,7 @@ function setupFrameHandling(page, forceDebug) {
4541
4664
  if (forceDebug) {
4542
4665
  console.log(formatLogMessage('debug', `Final cleanup results: ${cleanupResult.success ? 'success' : 'failed'}`));
4543
4666
  console.log(formatLogMessage('debug', `Browser closed: ${cleanupResult.browserClosed}, Temp files cleaned: ${cleanupResult.tempFilesCleanedCount || 0}, User data cleaned: ${cleanupResult.userDataCleaned}`));
4544
-
4667
+
4545
4668
  if (cleanupResult.errors.length > 0) {
4546
4669
  cleanupResult.errors.forEach(err => console.log(formatLogMessage('debug', `Cleanup error: ${err}`)));
4547
4670
  }
@@ -4549,10 +4672,10 @@ function setupFrameHandling(page, forceDebug) {
4549
4672
 
4550
4673
  // Final aggressive cleanup to catch any remaining temp files
4551
4674
  if (forceDebug) console.log(formatLogMessage('debug', 'Performing final aggressive temp file cleanup...'));
4552
- await cleanupChromeTempFiles({
4553
- includeSnapTemp: true,
4675
+ await cleanupChromeTempFiles({
4676
+ includeSnapTemp: true,
4554
4677
  forceDebug,
4555
- comprehensive: true
4678
+ comprehensive: true
4556
4679
  });
4557
4680
  await fastTimeout(TIMEOUTS.BROWSER_STABILIZE_DELAY); // Give filesystem time to sync
4558
4681
 
@@ -4591,6 +4714,24 @@ function setupFrameHandling(page, forceDebug) {
4591
4714
  if (ignoreCache && forceDebug) {
4592
4715
  console.log(messageColors.info('Cache:') + ` Smart caching was disabled`);
4593
4716
  }
4717
+ // DNS cache statistics
4718
+ const dnsStats = getDnsCacheStats();
4719
+ if (dnsStats.digHits + dnsStats.digMisses > 0 || dnsStats.whoisHits + dnsStats.whoisMisses > 0) {
4720
+ const parts = [];
4721
+ if (dnsStats.digHits + dnsStats.digMisses > 0) {
4722
+ parts.push(`${messageColors.success(dnsStats.digHits)} dig cached, ${messageColors.timing(dnsStats.digMisses)} fresh`);
4723
+ }
4724
+ if (dnsStats.whoisHits + dnsStats.whoisMisses > 0) {
4725
+ parts.push(`${messageColors.success(dnsStats.whoisHits)} whois cached, ${messageColors.timing(dnsStats.whoisMisses)} fresh`);
4726
+ }
4727
+ console.log(messageColors.info('DNS cache:') + ` ${parts.join(' | ')}`);
4728
+ if (dnsStats.freshDig.length > 0) {
4729
+ console.log(messageColors.info(' Fresh dig:') + ` ${dnsStats.freshDig.join(', ')}`);
4730
+ }
4731
+ if (dnsStats.freshWhois.length > 0) {
4732
+ console.log(messageColors.info(' Fresh whois:') + ` ${dnsStats.freshWhois.join(', ')}`);
4733
+ }
4734
+ }
4594
4735
  }
4595
4736
 
4596
4737
  // Clean process termination
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.58",
3
+ "version": "2.0.60",
4
4
  "description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
5
5
  "main": "nwss.js",
6
6
  "scripts": {
@@ -10,6 +10,7 @@
10
10
  "lint": "eslint *.js lib/*.js"
11
11
  },
12
12
  "dependencies": {
13
+ "ghost-cursor": "^1.4.2",
13
14
  "lru-cache": "^10.4.3",
14
15
  "p-limit": "^4.0.0",
15
16
  "psl": "^1.15.0",
@@ -48,6 +49,9 @@
48
49
  "url": "https://github.com/ryanbr/network-scanner/issues"
49
50
  },
50
51
  "homepage": "https://github.com/ryanbr/network-scanner",
52
+ "optionalDependencies": {
53
+ "puppeteer-core": ">=20.0.0"
54
+ },
51
55
  "devDependencies": {
52
56
  "eslint": "^10.0.2",
53
57
  "globals": "^16.3.0"