UNPKG

@fanboynz/network-scanner

Version:

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.

515 lines (441 loc) 17.5 kB
// === Dry Run Module (dry-run.js) === // Handles dry run mode functionality for network scanner const fs = require('fs'); const { messageColors, formatLogMessage } = require('./colorize'); // Constants for dry run collection keys const DRY_RUN_KEYS = { MATCHES: 'dryRunMatches', NET_TOOLS: 'dryRunNetTools', SEARCH_STRING: 'dryRunSearchString' }; /** * Initialize dry run collections for a matched domains map * @param {Map} matchedDomains - The matched domains map to initialize * @throws {Error} If matchedDomains is not a Map instance */ function initializeDryRunCollections(matchedDomains) { if (!(matchedDomains instanceof Map)) { throw new Error('matchedDomains must be a Map instance for dry-run mode'); } matchedDomains.set(DRY_RUN_KEYS.MATCHES, []); matchedDomains.set(DRY_RUN_KEYS.NET_TOOLS, []); matchedDomains.set(DRY_RUN_KEYS.SEARCH_STRING, new Map()); } /** * Validates match data object structure * @param {Object} matchData - Match data to validate * @throws {Error} If matchData is invalid */ function validateMatchData(matchData) { if (!matchData || typeof matchData !== 'object') { throw new Error('Match data must be an object'); } const requiredFields = ['regex', 'domain', 'resourceType', 'fullUrl']; for (const field of requiredFields) { if (!(field in matchData)) { throw new Error(`Match data missing required field: ${field}`); } } } /** * Validates nettools data object structure * @param {Object} netToolsData - NetTools data to validate * @throws {Error} If netToolsData is invalid */ function validateNetToolsData(netToolsData) { if (!netToolsData || typeof netToolsData !== 'object') { throw new Error('NetTools data must be an object'); } const requiredFields = ['domain', 'tool', 'matchType', 'matchedTerm']; for (const field of requiredFields) { if (!(field in netToolsData)) { throw new Error(`NetTools data missing required field: ${field}`); } } } /** * Add a match to dry run collections * @param {Map} matchedDomains - The matched domains map * @param {Object} matchData - Match data object * @throws {Error} If parameters are invalid */ function addDryRunMatch(matchedDomains, matchData) { if (!(matchedDomains instanceof Map)) { throw new Error('matchedDomains must be a Map instance'); } validateMatchData(matchData); if (!matchedDomains.has(DRY_RUN_KEYS.MATCHES)) { throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.'); } matchedDomains.get(DRY_RUN_KEYS.MATCHES).push({ ...matchData, timestamp: new Date().toISOString() }); } /** * Add a nettools result to dry run collections * @param {Map} matchedDomains - The matched domains map * @param {Object} netToolsData - NetTools result data * @throws {Error} If parameters are invalid */ function addDryRunNetTools(matchedDomains, netToolsData) { if (!(matchedDomains instanceof Map)) { throw new Error('matchedDomains must be a Map instance'); } validateNetToolsData(netToolsData); if (!matchedDomains.has(DRY_RUN_KEYS.NET_TOOLS)) { throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.'); } matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS).push({ ...netToolsData, timestamp: new Date().toISOString() }); } /** * Add a search string result to dry run collections * @param {Map} matchedDomains - The matched domains map * @param {string} url - The URL that was searched * @param {Object} searchResult - Search result data * @throws {Error} If parameters are invalid */ function addDryRunSearchString(matchedDomains, url, searchResult) { if (!(matchedDomains instanceof Map)) { throw new Error('matchedDomains must be a Map instance'); } if (!url || typeof url !== 'string') { throw new Error('URL must be a non-empty string'); } if (!searchResult || typeof searchResult !== 'object') { throw new Error('Search result must be an object'); } if (!matchedDomains.has(DRY_RUN_KEYS.SEARCH_STRING)) { throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.'); } matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING).set(url, { ...searchResult, timestamp: new Date().toISOString() }); } /** * Safely truncate long URLs for display * @param {string} url - URL to truncate * @param {number} maxLength - Maximum length to display * @returns {string} Truncated URL with ellipsis if needed */ function truncateUrl(url, maxLength = 80) { if (!url || url.length <= maxLength) { return url; } return url.substring(0, maxLength - 3) + '...'; } /** * Format search string match information * @param {Object} searchStringMatch - Search string match data * @returns {string} Formatted match description */ function formatSearchStringMatch(searchStringMatch) { if (!searchStringMatch) return null; const matchType = searchStringMatch.type || 'unknown'; const term = searchStringMatch.term || 'unknown'; return `${matchType} - "${term}"`; } /** * Generate adblock rule from domain and resource type * @param {string} domain - Domain name * @param {string} resourceType - Resource type (optional) * @returns {string} Formatted adblock rule */ function generateAdblockRule(domain, resourceType = null) { if (!domain) return ''; if (resourceType && resourceType !== 'other') { return `||${domain}^$${resourceType}`; } return `||${domain}^`; } /** * Outputs dry run results to console with formatted display * If outputFile is specified, also captures output for file writing * @param {string} url - The URL being processed * @param {Array} matchedItems - Array of matched items with regex, domain, and resource type * @param {Array} netToolsResults - Array of whois/dig results * @param {string} pageTitle - Title of the page (if available) * @param {string} outputFile - Output file path (optional) * @param {Array} dryRunOutput - Array to collect output lines for file writing */ function outputDryRunResults(url, matchedItems = [], netToolsResults = [], pageTitle = '', outputFile = null, dryRunOutput = []) { try { const lines = []; const truncatedUrl = truncateUrl(url); lines.push(`\n=== DRY RUN RESULTS === ${truncatedUrl}`); console.log(`\n${messageColors.scanning('=== DRY RUN RESULTS ===')} ${truncatedUrl}`); if (pageTitle && pageTitle.trim()) { const cleanTitle = pageTitle.trim().substring(0, 200); // Limit title length lines.push(`Title: ${cleanTitle}`); console.log(`${messageColors.info('Title:')} ${cleanTitle}`); } const totalMatches = matchedItems.length + netToolsResults.length; if (totalMatches === 0) { const noMatchMsg = `No matching rules found on ${truncatedUrl}`; lines.push(noMatchMsg); if (outputFile) { dryRunOutput.push(...lines); dryRunOutput.push(''); // Add empty line } console.log(messageColors.warn(noMatchMsg)); return; } lines.push(`Matches found: ${totalMatches}`); console.log(`${messageColors.success('Matches found:')} ${totalMatches}`); // Process regex matches matchedItems.forEach((item, index) => { try { lines.push(''); lines.push(`[${index + 1}] Regex Match:`); lines.push(` Pattern: ${item.regex || 'unknown'}`); lines.push(` Domain: ${item.domain || 'unknown'}`); lines.push(` Resource Type: ${item.resourceType || 'unknown'}`); lines.push(` Full URL: ${truncateUrl(item.fullUrl || '')}`); console.log(`\n${messageColors.highlight(`[${index + 1}]`)} ${messageColors.match('Regex Match:')}`); console.log(` Pattern: ${item.regex || 'unknown'}`); console.log(` Domain: ${item.domain || 'unknown'}`); console.log(` Resource Type: ${item.resourceType || 'unknown'}`); console.log(` Full URL: ${truncateUrl(item.fullUrl || '')}`); // Show blocked status if applicable if (item.wasBlocked) { lines.push(` Status: BLOCKED (even_blocked enabled)`); console.log(` ${messageColors.warn('Status:')} BLOCKED (even_blocked enabled)`); } // Show searchstring results if available if (item.searchStringMatch) { const matchDesc = formatSearchStringMatch(item.searchStringMatch); lines.push(` ? Searchstring Match: ${matchDesc}`); console.log(` ${messageColors.success('? Searchstring Match:')} ${matchDesc}`); } else if (item.searchStringChecked) { lines.push(` ? Searchstring: No matches found in content`); console.log(` ${messageColors.warn('? Searchstring:')} No matches found in content`); } // Generate adblock rule const adblockRule = generateAdblockRule(item.domain, item.resourceType); lines.push(` Adblock Rule: ${adblockRule}`); console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`); } catch (itemErr) { const errorMsg = `Error processing match item ${index + 1}: ${itemErr.message}`; lines.push(` Error: ${errorMsg}`); console.log(` ${messageColors.warn('Error:')} ${errorMsg}`); } }); // Process nettools results netToolsResults.forEach((result, index) => { try { const resultIndex = matchedItems.length + index + 1; lines.push(''); lines.push(`[${resultIndex}] NetTools Match:`); lines.push(` Domain: ${result.domain || 'unknown'}`); lines.push(` Tool: ${(result.tool || 'unknown').toUpperCase()}`); const matchDesc = `${result.matchType || 'unknown'} - "${result.matchedTerm || 'unknown'}"`; lines.push(` ? Match: ${matchDesc}`); if (result.details) { lines.push(` Details: ${result.details}`); } console.log(`\n${messageColors.highlight(`[${resultIndex}]`)} ${messageColors.match('NetTools Match:')}`); console.log(` Domain: ${result.domain || 'unknown'}`); console.log(` Tool: ${(result.tool || 'unknown').toUpperCase()}`); console.log(` ${messageColors.success('? Match:')} ${matchDesc}`); if (result.details) { console.log(` Details: ${result.details}`); } // Generate adblock rule for nettools matches const adblockRule = generateAdblockRule(result.domain); lines.push(` Adblock Rule: ${adblockRule}`); console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`); } catch (resultErr) { const errorMsg = `Error processing nettools result ${index + 1}: ${resultErr.message}`; lines.push(` Error: ${errorMsg}`); console.log(` ${messageColors.warn('Error:')} ${errorMsg}`); } }); // Store output for file writing if outputFile is specified if (outputFile) { dryRunOutput.push(...lines); dryRunOutput.push(''); // Add empty line between sites } } catch (outputErr) { const errorMsg = `Error in outputDryRunResults: ${outputErr.message}`; console.error(messageColors.error(errorMsg)); if (outputFile) { dryRunOutput.push(`Error: ${errorMsg}`); } } } /** * Process dry run results for a URL and output them * @param {string} currentUrl - The URL being processed * @param {Map} matchedDomains - The matched domains map with dry run collections * @param {Object} page - Puppeteer page object for getting title * @param {string} outputFile - Output file path (optional) * @param {Array} dryRunOutput - Array to collect output lines for file writing * @param {boolean} forceDebug - Debug logging flag * @returns {Object} Dry run result summary */ async function processDryRunResults(currentUrl, matchedDomains, page, outputFile = null, dryRunOutput = [], forceDebug = false) { try { // Validate inputs if (!currentUrl || typeof currentUrl !== 'string') { throw new Error('currentUrl must be a non-empty string'); } if (!(matchedDomains instanceof Map)) { throw new Error('matchedDomains must be a Map instance'); } // Get page title for dry run output with error handling let pageTitle = ''; try { if (page && typeof page.title === 'function') { pageTitle = await page.title(); } } catch (titleErr) { if (forceDebug) { console.log(formatLogMessage('debug', `Failed to get page title for ${currentUrl}: ${titleErr.message}`)); } pageTitle = 'Title unavailable'; } // Get collected matches with safe fallbacks const dryRunMatches = matchedDomains.get(DRY_RUN_KEYS.MATCHES) || []; const dryRunNetTools = matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS) || []; const dryRunSearchString = matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING) || new Map(); // Enhance matches with searchstring results const enhancedMatches = dryRunMatches.map((match, index) => { try { const searchResult = dryRunSearchString.get(match.fullUrl); return { ...match, searchStringMatch: searchResult && searchResult.matched ? searchResult : null, searchStringChecked: Boolean(match.needsSearchStringCheck) }; } catch (enhanceErr) { if (forceDebug) { console.log(formatLogMessage('debug', `Error enhancing match ${index}: ${enhanceErr.message}`)); } return { ...match, searchStringMatch: null, searchStringChecked: false }; } }); outputDryRunResults(currentUrl, enhancedMatches, dryRunNetTools, pageTitle, outputFile, dryRunOutput); const totalMatches = enhancedMatches.length + dryRunNetTools.length; return { success: true, matchCount: totalMatches, enhancedMatches, netToolsResults: dryRunNetTools, pageTitle, regexMatches: enhancedMatches.length, netToolsMatches: dryRunNetTools.length }; } catch (processErr) { const errorMsg = `Error processing dry run results for ${currentUrl}: ${processErr.message}`; console.error(messageColors.error(errorMsg)); if (forceDebug) { console.log(formatLogMessage('debug', `Stack trace: ${processErr.stack}`)); } return { success: false, error: errorMsg, matchCount: 0, enhancedMatches: [], netToolsResults: [], pageTitle: '', regexMatches: 0, netToolsMatches: 0 }; } } /** * Write dry run output to file with enhanced error handling * @param {string} outputFile - Output file path * @param {Array} dryRunOutput - Array of output lines * @param {boolean} silentMode - Silent mode flag * @returns {Object} Operation result with details */ function writeDryRunOutput(outputFile, dryRunOutput, silentMode = false) { try { if (!outputFile || typeof outputFile !== 'string') { return { success: false, error: 'Invalid output file path' }; } if (!Array.isArray(dryRunOutput) || dryRunOutput.length === 0) { if (!silentMode) { console.log(messageColors.info('No dry run output to write')); } return { success: true, written: false, reason: 'No output to write' }; } const dryRunContent = dryRunOutput.join('\n'); // Ensure output directory exists const path = require('path'); const outputDir = path.dirname(outputFile); if (outputDir !== '.') { fs.mkdirSync(outputDir, { recursive: true }); } fs.writeFileSync(outputFile, dryRunContent); if (!silentMode) { console.log(`${messageColors.fileOp('?? Dry run results saved to:')} ${outputFile}`); } return { success: true, written: true, file: outputFile, lines: dryRunOutput.length, bytes: Buffer.byteLength(dryRunContent, 'utf8') }; } catch (writeErr) { const errorMsg = `Failed to write dry run output to ${outputFile}: ${writeErr.message}`; console.error(`? ${errorMsg}`); return { success: false, error: errorMsg, written: false }; } } /** * Get statistics from dry run collections * @param {Map} matchedDomains - The matched domains map * @returns {Object} Statistics object */ function getDryRunStats(matchedDomains) { if (!(matchedDomains instanceof Map)) { return { error: 'Invalid matchedDomains Map' }; } const matches = matchedDomains.get(DRY_RUN_KEYS.MATCHES) || []; const netTools = matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS) || []; const searchStrings = matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING) || new Map(); return { totalMatches: matches.length + netTools.length, regexMatches: matches.length, netToolsMatches: netTools.length, searchStringResults: searchStrings.size, domains: new Set([ ...matches.map(m => m.domain).filter(Boolean), ...netTools.map(n => n.domain).filter(Boolean) ]).size }; } module.exports = { // Constants DRY_RUN_KEYS, // Core functions initializeDryRunCollections, addDryRunMatch, addDryRunNetTools, addDryRunSearchString, processDryRunResults, writeDryRunOutput, // Utility functions getDryRunStats, validateMatchData, validateNetToolsData, truncateUrl, formatSearchStringMatch, generateAdblockRule, outputDryRunResults };