@fanboynz/network-scanner 2.0.21 → 2.0.22

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.
Files changed (3) hide show
  1. package/lib/dry-run.js +515 -0
  2. package/nwss.js +18 -141
  3. package/package.json +1 -1
package/lib/dry-run.js ADDED
@@ -0,0 +1,515 @@
1
+ // === Dry Run Module (dry-run.js) ===
2
+ // Handles dry run mode functionality for network scanner
3
+
4
+ const fs = require('fs');
5
+ const { messageColors, formatLogMessage } = require('./colorize');
6
+
7
+ // Constants for dry run collection keys
8
+ const DRY_RUN_KEYS = {
9
+ MATCHES: 'dryRunMatches',
10
+ NET_TOOLS: 'dryRunNetTools',
11
+ SEARCH_STRING: 'dryRunSearchString'
12
+ };
13
+
14
+ /**
15
+ * Initialize dry run collections for a matched domains map
16
+ * @param {Map} matchedDomains - The matched domains map to initialize
17
+ * @throws {Error} If matchedDomains is not a Map instance
18
+ */
19
+ function initializeDryRunCollections(matchedDomains) {
20
+ if (!(matchedDomains instanceof Map)) {
21
+ throw new Error('matchedDomains must be a Map instance for dry-run mode');
22
+ }
23
+
24
+ matchedDomains.set(DRY_RUN_KEYS.MATCHES, []);
25
+ matchedDomains.set(DRY_RUN_KEYS.NET_TOOLS, []);
26
+ matchedDomains.set(DRY_RUN_KEYS.SEARCH_STRING, new Map());
27
+ }
28
+
29
+ /**
30
+ * Validates match data object structure
31
+ * @param {Object} matchData - Match data to validate
32
+ * @throws {Error} If matchData is invalid
33
+ */
34
+ function validateMatchData(matchData) {
35
+ if (!matchData || typeof matchData !== 'object') {
36
+ throw new Error('Match data must be an object');
37
+ }
38
+
39
+ const requiredFields = ['regex', 'domain', 'resourceType', 'fullUrl'];
40
+ for (const field of requiredFields) {
41
+ if (!(field in matchData)) {
42
+ throw new Error(`Match data missing required field: ${field}`);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Validates nettools data object structure
49
+ * @param {Object} netToolsData - NetTools data to validate
50
+ * @throws {Error} If netToolsData is invalid
51
+ */
52
+ function validateNetToolsData(netToolsData) {
53
+ if (!netToolsData || typeof netToolsData !== 'object') {
54
+ throw new Error('NetTools data must be an object');
55
+ }
56
+
57
+ const requiredFields = ['domain', 'tool', 'matchType', 'matchedTerm'];
58
+ for (const field of requiredFields) {
59
+ if (!(field in netToolsData)) {
60
+ throw new Error(`NetTools data missing required field: ${field}`);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Add a match to dry run collections
67
+ * @param {Map} matchedDomains - The matched domains map
68
+ * @param {Object} matchData - Match data object
69
+ * @throws {Error} If parameters are invalid
70
+ */
71
+ function addDryRunMatch(matchedDomains, matchData) {
72
+ if (!(matchedDomains instanceof Map)) {
73
+ throw new Error('matchedDomains must be a Map instance');
74
+ }
75
+
76
+ validateMatchData(matchData);
77
+
78
+ if (!matchedDomains.has(DRY_RUN_KEYS.MATCHES)) {
79
+ throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.');
80
+ }
81
+
82
+ matchedDomains.get(DRY_RUN_KEYS.MATCHES).push({
83
+ ...matchData,
84
+ timestamp: new Date().toISOString()
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Add a nettools result to dry run collections
90
+ * @param {Map} matchedDomains - The matched domains map
91
+ * @param {Object} netToolsData - NetTools result data
92
+ * @throws {Error} If parameters are invalid
93
+ */
94
+ function addDryRunNetTools(matchedDomains, netToolsData) {
95
+ if (!(matchedDomains instanceof Map)) {
96
+ throw new Error('matchedDomains must be a Map instance');
97
+ }
98
+
99
+ validateNetToolsData(netToolsData);
100
+
101
+ if (!matchedDomains.has(DRY_RUN_KEYS.NET_TOOLS)) {
102
+ throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.');
103
+ }
104
+
105
+ matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS).push({
106
+ ...netToolsData,
107
+ timestamp: new Date().toISOString()
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Add a search string result to dry run collections
113
+ * @param {Map} matchedDomains - The matched domains map
114
+ * @param {string} url - The URL that was searched
115
+ * @param {Object} searchResult - Search result data
116
+ * @throws {Error} If parameters are invalid
117
+ */
118
+ function addDryRunSearchString(matchedDomains, url, searchResult) {
119
+ if (!(matchedDomains instanceof Map)) {
120
+ throw new Error('matchedDomains must be a Map instance');
121
+ }
122
+
123
+ if (!url || typeof url !== 'string') {
124
+ throw new Error('URL must be a non-empty string');
125
+ }
126
+
127
+ if (!searchResult || typeof searchResult !== 'object') {
128
+ throw new Error('Search result must be an object');
129
+ }
130
+
131
+ if (!matchedDomains.has(DRY_RUN_KEYS.SEARCH_STRING)) {
132
+ throw new Error('Dry run collections not initialized. Call initializeDryRunCollections first.');
133
+ }
134
+
135
+ matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING).set(url, {
136
+ ...searchResult,
137
+ timestamp: new Date().toISOString()
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Safely truncate long URLs for display
143
+ * @param {string} url - URL to truncate
144
+ * @param {number} maxLength - Maximum length to display
145
+ * @returns {string} Truncated URL with ellipsis if needed
146
+ */
147
+ function truncateUrl(url, maxLength = 80) {
148
+ if (!url || url.length <= maxLength) {
149
+ return url;
150
+ }
151
+ return url.substring(0, maxLength - 3) + '...';
152
+ }
153
+
154
+ /**
155
+ * Format search string match information
156
+ * @param {Object} searchStringMatch - Search string match data
157
+ * @returns {string} Formatted match description
158
+ */
159
+ function formatSearchStringMatch(searchStringMatch) {
160
+ if (!searchStringMatch) return null;
161
+
162
+ const matchType = searchStringMatch.type || 'unknown';
163
+ const term = searchStringMatch.term || 'unknown';
164
+ return `${matchType} - "${term}"`;
165
+ }
166
+
167
+ /**
168
+ * Generate adblock rule from domain and resource type
169
+ * @param {string} domain - Domain name
170
+ * @param {string} resourceType - Resource type (optional)
171
+ * @returns {string} Formatted adblock rule
172
+ */
173
+ function generateAdblockRule(domain, resourceType = null) {
174
+ if (!domain) return '';
175
+
176
+ if (resourceType && resourceType !== 'other') {
177
+ return `||${domain}^${resourceType}`;
178
+ }
179
+ return `||${domain}^`;
180
+ }
181
+
182
+ /**
183
+ * Outputs dry run results to console with formatted display
184
+ * If outputFile is specified, also captures output for file writing
185
+ * @param {string} url - The URL being processed
186
+ * @param {Array} matchedItems - Array of matched items with regex, domain, and resource type
187
+ * @param {Array} netToolsResults - Array of whois/dig results
188
+ * @param {string} pageTitle - Title of the page (if available)
189
+ * @param {string} outputFile - Output file path (optional)
190
+ * @param {Array} dryRunOutput - Array to collect output lines for file writing
191
+ */
192
+ function outputDryRunResults(url, matchedItems = [], netToolsResults = [], pageTitle = '', outputFile = null, dryRunOutput = []) {
193
+ try {
194
+ const lines = [];
195
+ const truncatedUrl = truncateUrl(url);
196
+
197
+ lines.push(`\n=== DRY RUN RESULTS === ${truncatedUrl}`);
198
+ console.log(`\n${messageColors.scanning('=== DRY RUN RESULTS ===')} ${truncatedUrl}`);
199
+
200
+ if (pageTitle && pageTitle.trim()) {
201
+ const cleanTitle = pageTitle.trim().substring(0, 200); // Limit title length
202
+ lines.push(`Title: ${cleanTitle}`);
203
+ console.log(`${messageColors.info('Title:')} ${cleanTitle}`);
204
+ }
205
+
206
+ const totalMatches = matchedItems.length + netToolsResults.length;
207
+
208
+ if (totalMatches === 0) {
209
+ const noMatchMsg = `No matching rules found on ${truncatedUrl}`;
210
+ lines.push(noMatchMsg);
211
+
212
+ if (outputFile) {
213
+ dryRunOutput.push(...lines);
214
+ dryRunOutput.push(''); // Add empty line
215
+ }
216
+ console.log(messageColors.warn(noMatchMsg));
217
+ return;
218
+ }
219
+
220
+ lines.push(`Matches found: ${totalMatches}`);
221
+ console.log(`${messageColors.success('Matches found:')} ${totalMatches}`);
222
+
223
+ // Process regex matches
224
+ matchedItems.forEach((item, index) => {
225
+ try {
226
+ lines.push('');
227
+ lines.push(`[${index + 1}] Regex Match:`);
228
+ lines.push(` Pattern: ${item.regex || 'unknown'}`);
229
+ lines.push(` Domain: ${item.domain || 'unknown'}`);
230
+ lines.push(` Resource Type: ${item.resourceType || 'unknown'}`);
231
+ lines.push(` Full URL: ${truncateUrl(item.fullUrl || '')}`);
232
+
233
+ console.log(`\n${messageColors.highlight(`[${index + 1}]`)} ${messageColors.match('Regex Match:')}`);
234
+ console.log(` Pattern: ${item.regex || 'unknown'}`);
235
+ console.log(` Domain: ${item.domain || 'unknown'}`);
236
+ console.log(` Resource Type: ${item.resourceType || 'unknown'}`);
237
+ console.log(` Full URL: ${truncateUrl(item.fullUrl || '')}`);
238
+
239
+ // Show blocked status if applicable
240
+ if (item.wasBlocked) {
241
+ lines.push(` Status: BLOCKED (even_blocked enabled)`);
242
+ console.log(` ${messageColors.warn('Status:')} BLOCKED (even_blocked enabled)`);
243
+ }
244
+
245
+ // Show searchstring results if available
246
+ if (item.searchStringMatch) {
247
+ const matchDesc = formatSearchStringMatch(item.searchStringMatch);
248
+ lines.push(` ? Searchstring Match: ${matchDesc}`);
249
+ console.log(` ${messageColors.success('? Searchstring Match:')} ${matchDesc}`);
250
+ } else if (item.searchStringChecked) {
251
+ lines.push(` ? Searchstring: No matches found in content`);
252
+ console.log(` ${messageColors.warn('? Searchstring:')} No matches found in content`);
253
+ }
254
+
255
+ // Generate adblock rule
256
+ const adblockRule = generateAdblockRule(item.domain, item.resourceType);
257
+ lines.push(` Adblock Rule: ${adblockRule}`);
258
+ console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
259
+
260
+ } catch (itemErr) {
261
+ const errorMsg = `Error processing match item ${index + 1}: ${itemErr.message}`;
262
+ lines.push(` Error: ${errorMsg}`);
263
+ console.log(` ${messageColors.warn('Error:')} ${errorMsg}`);
264
+ }
265
+ });
266
+
267
+ // Process nettools results
268
+ netToolsResults.forEach((result, index) => {
269
+ try {
270
+ const resultIndex = matchedItems.length + index + 1;
271
+ lines.push('');
272
+ lines.push(`[${resultIndex}] NetTools Match:`);
273
+ lines.push(` Domain: ${result.domain || 'unknown'}`);
274
+ lines.push(` Tool: ${(result.tool || 'unknown').toUpperCase()}`);
275
+
276
+ const matchDesc = `${result.matchType || 'unknown'} - "${result.matchedTerm || 'unknown'}"`;
277
+ lines.push(` ? Match: ${matchDesc}`);
278
+
279
+ if (result.details) {
280
+ lines.push(` Details: ${result.details}`);
281
+ }
282
+
283
+ console.log(`\n${messageColors.highlight(`[${resultIndex}]`)} ${messageColors.match('NetTools Match:')}`);
284
+ console.log(` Domain: ${result.domain || 'unknown'}`);
285
+ console.log(` Tool: ${(result.tool || 'unknown').toUpperCase()}`);
286
+ console.log(` ${messageColors.success('? Match:')} ${matchDesc}`);
287
+
288
+ if (result.details) {
289
+ console.log(` Details: ${result.details}`);
290
+ }
291
+
292
+ // Generate adblock rule for nettools matches
293
+ const adblockRule = generateAdblockRule(result.domain);
294
+ lines.push(` Adblock Rule: ${adblockRule}`);
295
+ console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
296
+
297
+ } catch (resultErr) {
298
+ const errorMsg = `Error processing nettools result ${index + 1}: ${resultErr.message}`;
299
+ lines.push(` Error: ${errorMsg}`);
300
+ console.log(` ${messageColors.warn('Error:')} ${errorMsg}`);
301
+ }
302
+ });
303
+
304
+ // Store output for file writing if outputFile is specified
305
+ if (outputFile) {
306
+ dryRunOutput.push(...lines);
307
+ dryRunOutput.push(''); // Add empty line between sites
308
+ }
309
+
310
+ } catch (outputErr) {
311
+ const errorMsg = `Error in outputDryRunResults: ${outputErr.message}`;
312
+ console.error(messageColors.error(errorMsg));
313
+ if (outputFile) {
314
+ dryRunOutput.push(`Error: ${errorMsg}`);
315
+ }
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Process dry run results for a URL and output them
321
+ * @param {string} currentUrl - The URL being processed
322
+ * @param {Map} matchedDomains - The matched domains map with dry run collections
323
+ * @param {Object} page - Puppeteer page object for getting title
324
+ * @param {string} outputFile - Output file path (optional)
325
+ * @param {Array} dryRunOutput - Array to collect output lines for file writing
326
+ * @param {boolean} forceDebug - Debug logging flag
327
+ * @returns {Object} Dry run result summary
328
+ */
329
+ async function processDryRunResults(currentUrl, matchedDomains, page, outputFile = null, dryRunOutput = [], forceDebug = false) {
330
+ try {
331
+ // Validate inputs
332
+ if (!currentUrl || typeof currentUrl !== 'string') {
333
+ throw new Error('currentUrl must be a non-empty string');
334
+ }
335
+
336
+ if (!(matchedDomains instanceof Map)) {
337
+ throw new Error('matchedDomains must be a Map instance');
338
+ }
339
+
340
+ // Get page title for dry run output with error handling
341
+ let pageTitle = '';
342
+ try {
343
+ if (page && typeof page.title === 'function') {
344
+ pageTitle = await page.title();
345
+ }
346
+ } catch (titleErr) {
347
+ if (forceDebug) {
348
+ console.log(formatLogMessage('debug', `Failed to get page title for ${currentUrl}: ${titleErr.message}`));
349
+ }
350
+ pageTitle = 'Title unavailable';
351
+ }
352
+
353
+ // Get collected matches with safe fallbacks
354
+ const dryRunMatches = matchedDomains.get(DRY_RUN_KEYS.MATCHES) || [];
355
+ const dryRunNetTools = matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS) || [];
356
+ const dryRunSearchString = matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING) || new Map();
357
+
358
+ // Enhance matches with searchstring results
359
+ const enhancedMatches = dryRunMatches.map((match, index) => {
360
+ try {
361
+ const searchResult = dryRunSearchString.get(match.fullUrl);
362
+ return {
363
+ ...match,
364
+ searchStringMatch: searchResult && searchResult.matched ? searchResult : null,
365
+ searchStringChecked: Boolean(match.needsSearchStringCheck)
366
+ };
367
+ } catch (enhanceErr) {
368
+ if (forceDebug) {
369
+ console.log(formatLogMessage('debug', `Error enhancing match ${index}: ${enhanceErr.message}`));
370
+ }
371
+ return {
372
+ ...match,
373
+ searchStringMatch: null,
374
+ searchStringChecked: false
375
+ };
376
+ }
377
+ });
378
+
379
+ outputDryRunResults(currentUrl, enhancedMatches, dryRunNetTools, pageTitle, outputFile, dryRunOutput);
380
+
381
+ const totalMatches = enhancedMatches.length + dryRunNetTools.length;
382
+
383
+ return {
384
+ success: true,
385
+ matchCount: totalMatches,
386
+ enhancedMatches,
387
+ netToolsResults: dryRunNetTools,
388
+ pageTitle,
389
+ regexMatches: enhancedMatches.length,
390
+ netToolsMatches: dryRunNetTools.length
391
+ };
392
+
393
+ } catch (processErr) {
394
+ const errorMsg = `Error processing dry run results for ${currentUrl}: ${processErr.message}`;
395
+ console.error(messageColors.error(errorMsg));
396
+
397
+ if (forceDebug) {
398
+ console.log(formatLogMessage('debug', `Stack trace: ${processErr.stack}`));
399
+ }
400
+
401
+ return {
402
+ success: false,
403
+ error: errorMsg,
404
+ matchCount: 0,
405
+ enhancedMatches: [],
406
+ netToolsResults: [],
407
+ pageTitle: '',
408
+ regexMatches: 0,
409
+ netToolsMatches: 0
410
+ };
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Write dry run output to file with enhanced error handling
416
+ * @param {string} outputFile - Output file path
417
+ * @param {Array} dryRunOutput - Array of output lines
418
+ * @param {boolean} silentMode - Silent mode flag
419
+ * @returns {Object} Operation result with details
420
+ */
421
+ function writeDryRunOutput(outputFile, dryRunOutput, silentMode = false) {
422
+ try {
423
+ if (!outputFile || typeof outputFile !== 'string') {
424
+ return { success: false, error: 'Invalid output file path' };
425
+ }
426
+
427
+ if (!Array.isArray(dryRunOutput) || dryRunOutput.length === 0) {
428
+ if (!silentMode) {
429
+ console.log(messageColors.info('No dry run output to write'));
430
+ }
431
+ return { success: true, written: false, reason: 'No output to write' };
432
+ }
433
+
434
+ const dryRunContent = dryRunOutput.join('\n');
435
+
436
+ // Ensure output directory exists
437
+ const path = require('path');
438
+ const outputDir = path.dirname(outputFile);
439
+ if (outputDir !== '.' && !fs.existsSync(outputDir)) {
440
+ fs.mkdirSync(outputDir, { recursive: true });
441
+ }
442
+
443
+ fs.writeFileSync(outputFile, dryRunContent);
444
+
445
+ if (!silentMode) {
446
+ console.log(`${messageColors.fileOp('?? Dry run results saved to:')} ${outputFile}`);
447
+ }
448
+
449
+ return {
450
+ success: true,
451
+ written: true,
452
+ file: outputFile,
453
+ lines: dryRunOutput.length,
454
+ bytes: Buffer.byteLength(dryRunContent, 'utf8')
455
+ };
456
+
457
+ } catch (writeErr) {
458
+ const errorMsg = `Failed to write dry run output to ${outputFile}: ${writeErr.message}`;
459
+ console.error(`? ${errorMsg}`);
460
+
461
+ return {
462
+ success: false,
463
+ error: errorMsg,
464
+ written: false
465
+ };
466
+ }
467
+ }
468
+
469
+ /**
470
+ * Get statistics from dry run collections
471
+ * @param {Map} matchedDomains - The matched domains map
472
+ * @returns {Object} Statistics object
473
+ */
474
+ function getDryRunStats(matchedDomains) {
475
+ if (!(matchedDomains instanceof Map)) {
476
+ return { error: 'Invalid matchedDomains Map' };
477
+ }
478
+
479
+ const matches = matchedDomains.get(DRY_RUN_KEYS.MATCHES) || [];
480
+ const netTools = matchedDomains.get(DRY_RUN_KEYS.NET_TOOLS) || [];
481
+ const searchStrings = matchedDomains.get(DRY_RUN_KEYS.SEARCH_STRING) || new Map();
482
+
483
+ return {
484
+ totalMatches: matches.length + netTools.length,
485
+ regexMatches: matches.length,
486
+ netToolsMatches: netTools.length,
487
+ searchStringResults: searchStrings.size,
488
+ domains: new Set([
489
+ ...matches.map(m => m.domain).filter(Boolean),
490
+ ...netTools.map(n => n.domain).filter(Boolean)
491
+ ]).size
492
+ };
493
+ }
494
+
495
+ module.exports = {
496
+ // Constants
497
+ DRY_RUN_KEYS,
498
+
499
+ // Core functions
500
+ initializeDryRunCollections,
501
+ addDryRunMatch,
502
+ addDryRunNetTools,
503
+ addDryRunSearchString,
504
+ processDryRunResults,
505
+ writeDryRunOutput,
506
+
507
+ // Utility functions
508
+ getDryRunStats,
509
+ validateMatchData,
510
+ validateNetToolsData,
511
+ truncateUrl,
512
+ formatSearchStringMatch,
513
+ generateAdblockRule,
514
+ outputDryRunResults
515
+ };
package/nwss.js CHANGED
@@ -1,4 +1,4 @@
1
- // === Network scanner script (nwss.js) v2.0.21 ===
1
+ // === Network scanner script (nwss.js) v2.0.22 ===
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
@@ -44,6 +44,8 @@ const { performPageInteraction, createInteractionConfig } = require('./lib/inter
44
44
  const { createGlobalHelpers, getTotalDomainsSkipped, getDetectedDomainsCount } = require('./lib/domain-cache');
45
45
  const { createSmartCache } = require('./lib/smart-cache'); // Smart cache system
46
46
  const { clearPersistentCache } = require('./lib/smart-cache');
47
+ // Dry run functionality
48
+ const { initializeDryRunCollections, addDryRunMatch, addDryRunNetTools, processDryRunResults, writeDryRunOutput } = require('./lib/dry-run');
47
49
  // Enhanced site data clearing functionality
48
50
  const { clearSiteData } = require('./lib/clear_sitedata');
49
51
 
@@ -130,7 +132,7 @@ const { navigateWithRedirectHandling, handleRedirectTimeout } = require('./lib/r
130
132
  const { monitorBrowserHealth, isBrowserHealthy, isQuicklyResponsive, performGroupWindowCleanup, performRealtimeWindowCleanup, trackPageForRealtime, updatePageUsage, cleanupPageBeforeReload } = require('./lib/browserhealth');
131
133
 
132
134
  // --- Script Configuration & Constants ---
133
- const VERSION = '2.0.21'; // Script version
135
+ const VERSION = '2.0.22'; // Script version
134
136
 
135
137
  // get startTime
136
138
  const startTime = Date.now();
@@ -953,103 +955,6 @@ function shouldEnableCDPForUrl(url, cdpSpecificList) {
953
955
  }
954
956
  }
955
957
 
956
- /**
957
- * Outputs dry run results to console with formatted display
958
- * If outputFile is specified, also captures output for file writing
959
- * @param {string} url - The URL being processed
960
- * @param {Array} matchedItems - Array of matched items with regex, domain, and resource type
961
- * @param {Array} netToolsResults - Array of whois/dig results
962
- * @param {string} pageTitle - Title of the page (if available)
963
- */
964
- function outputDryRunResults(url, matchedItems, netToolsResults, pageTitle) {
965
- const lines = [];
966
-
967
- lines.push(`\n=== DRY RUN RESULTS === ${url}`);
968
-
969
- console.log(`\n${messageColors.scanning('=== DRY RUN RESULTS ===')} ${url}`);
970
-
971
- if (pageTitle && pageTitle.trim()) {
972
- lines.push(`Title: ${pageTitle.trim()}`);
973
- console.log(`${messageColors.info('Title:')} ${pageTitle.trim()}`);
974
- }
975
-
976
- if (matchedItems.length === 0 && netToolsResults.length === 0) {
977
- lines.push(`No matching rules found on ${url}`);
978
-
979
- // Store output for file writing if outputFile is specified
980
- if (outputFile) {
981
- dryRunOutput.push(...lines);
982
- dryRunOutput.push(''); // Add empty line
983
- }
984
- console.log(messageColors.warn(`No matching rules found on ${url}`));
985
- return;
986
- }
987
-
988
- const totalMatches = matchedItems.length + netToolsResults.length;
989
- lines.push(`Matches found: ${totalMatches}`);
990
- console.log(`${messageColors.success('Matches found:')} ${totalMatches}`);
991
-
992
- matchedItems.forEach((item, index) => {
993
- lines.push('');
994
- lines.push(`[${index + 1}] Regex Match:`);
995
- lines.push(` Pattern: ${item.regex}`);
996
- lines.push(` Domain: ${item.domain}`);
997
- lines.push(` Resource Type: ${item.resourceType}`);
998
- lines.push(` Full URL: ${item.fullUrl}`);
999
-
1000
- console.log(`\n${messageColors.highlight(`[${index + 1}]`)} ${messageColors.match('Regex Match:')}`);
1001
- console.log(` Pattern: ${item.regex}`);
1002
- console.log(` Domain: ${item.domain}`);
1003
- console.log(` Resource Type: ${item.resourceType}`);
1004
- console.log(` Full URL: ${item.fullUrl}`);
1005
-
1006
- // Show searchstring results if available
1007
- if (item.searchStringMatch) {
1008
- lines.push(` ✓ Searchstring Match: ${item.searchStringMatch.type} - "${item.searchStringMatch.term}"`);
1009
- console.log(` ${messageColors.success('✓ Searchstring Match:')} ${item.searchStringMatch.type} - "${item.searchStringMatch.term}"`);
1010
- } else if (item.searchStringChecked) {
1011
- lines.push(` ✗ Searchstring: No matches found in content`);
1012
- console.log(` ${messageColors.warn('✗ Searchstring:')} No matches found in content`);
1013
- }
1014
-
1015
- // Generate adblock rule
1016
- const adblockRule = `||${item.domain}^$${item.resourceType}`;
1017
- lines.push(` Adblock Rule: ${adblockRule}`);
1018
- console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
1019
- });
1020
-
1021
- // Display nettools results
1022
- netToolsResults.forEach((result, index) => {
1023
- const resultIndex = matchedItems.length + index + 1;
1024
- lines.push('');
1025
- lines.push(`[${resultIndex}] NetTools Match:`);
1026
- lines.push(` Domain: ${result.domain}`);
1027
- lines.push(` Tool: ${result.tool.toUpperCase()}`);
1028
- lines.push(` ✓ Match: ${result.matchType} - "${result.matchedTerm}"`);
1029
- if (result.details) {
1030
- lines.push(` Details: ${result.details}`);
1031
- }
1032
- console.log(`\n${messageColors.highlight(`[${resultIndex}]`)} ${messageColors.match('NetTools Match:')}`);
1033
- console.log(` Domain: ${result.domain}`);
1034
- console.log(` Tool: ${result.tool.toUpperCase()}`);
1035
- console.log(` ${messageColors.success('✓ Match:')} ${result.matchType} - "${result.matchedTerm}"`);
1036
- if (result.details) {
1037
- console.log(` Details: ${result.details}`);
1038
- }
1039
-
1040
- // Generate adblock rule for nettools matches
1041
- const adblockRule = `||${result.domain}^`;
1042
- lines.push(` Adblock Rule: ${adblockRule}`);
1043
- console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
1044
- });
1045
-
1046
- // Store output for file writing if outputFile is specified
1047
- if (outputFile) {
1048
- dryRunOutput.push(...lines);
1049
- dryRunOutput.push(''); // Add empty line between sites
1050
- }
1051
- }
1052
-
1053
958
  /**
1054
959
  * Helper function to check if a URL should be processed (valid HTTP/HTTPS)
1055
960
  * @param {string} url - URL to validate
@@ -1543,9 +1448,7 @@ function setupFrameHandling(page, forceDebug) {
1543
1448
 
1544
1449
  // Initialize dry run matches collection
1545
1450
  if (dryRunMode) {
1546
- matchedDomains.set('dryRunMatches', []);
1547
- matchedDomains.set('dryRunNetTools', []);
1548
- matchedDomains.set('dryRunSearchString', new Map()); // Map URL to search results
1451
+ initializeDryRunCollections(matchedDomains);
1549
1452
  }
1550
1453
  const timeout = siteConfig.timeout || TIMEOUTS.DEFAULT_PAGE;
1551
1454
 
@@ -2434,7 +2337,7 @@ function setupFrameHandling(page, forceDebug) {
2434
2337
  const allowedResourceTypes = siteConfig.resourceTypes;
2435
2338
  if (!allowedResourceTypes || !Array.isArray(allowedResourceTypes) || allowedResourceTypes.includes(resourceType)) {
2436
2339
  if (dryRunMode) {
2437
- matchedDomains.get('dryRunMatches').push({
2340
+ addDryRunMatch(matchedDomains, {
2438
2341
  regex: matchedRegexPattern,
2439
2342
  domain: reqDomain,
2440
2343
  resourceType: resourceType,
@@ -2598,7 +2501,7 @@ function setupFrameHandling(page, forceDebug) {
2598
2501
  // If NO searchstring AND NO nettools are defined, match immediately (existing behavior)
2599
2502
  if (!hasSearchString && !hasSearchStringAnd && !hasNetTools) {
2600
2503
  if (dryRunMode) {
2601
- matchedDomains.get('dryRunMatches').push({
2504
+ addDryRunMatch(matchedDomains, {
2602
2505
  regex: matchedRegexPattern,
2603
2506
  domain: reqDomain,
2604
2507
  resourceType: resourceType,
@@ -2641,8 +2544,7 @@ function setupFrameHandling(page, forceDebug) {
2641
2544
  }
2642
2545
 
2643
2546
  if (dryRunMode) {
2644
- // For dry run, we'll collect the domain for nettools checking
2645
- matchedDomains.get('dryRunMatches').push({
2547
+ addDryRunMatch(matchedDomains, {
2646
2548
  regex: matchedRegexPattern,
2647
2549
  domain: reqDomain,
2648
2550
  resourceType: resourceType,
@@ -2722,7 +2624,7 @@ function setupFrameHandling(page, forceDebug) {
2722
2624
  console.log(formatLogMessage('debug', `${reqUrl} matched regex ${matchedRegexPattern} and resourceType ${resourceType}, queued for ${searchType} content search`));
2723
2625
  }
2724
2626
  if (dryRunMode) {
2725
- matchedDomains.get('dryRunMatches').push({
2627
+ addDryRunMatch(matchedDomains, {
2726
2628
  regex: matchedRegexPattern,
2727
2629
  domain: reqDomain,
2728
2630
  resourceType: resourceType,
@@ -3433,38 +3335,18 @@ function setupFrameHandling(page, forceDebug) {
3433
3335
  updatePageUsage(page, false);
3434
3336
 
3435
3337
  if (dryRunMode) {
3436
- // Get page title for dry run output
3437
- let pageTitle = '';
3438
- try {
3439
- pageTitle = await page.title();
3440
- } catch (titleErr) {
3441
- if (forceDebug) {
3442
- console.log(formatLogMessage('debug', `Failed to get page title for ${currentUrl}: ${titleErr.message}`));
3443
- }
3338
+ // Process dry run results using the module
3339
+ const dryRunResult = await processDryRunResults(currentUrl, matchedDomains, page, outputFile, dryRunOutput, forceDebug);
3340
+
3341
+ if (!dryRunResult.success) {
3342
+ console.warn(messageColors.warn(`Dry run processing failed for ${currentUrl}: ${dryRunResult.error}`));
3444
3343
  }
3445
3344
 
3446
- // Get collected matches and enhance with searchstring results
3447
- const dryRunMatches = matchedDomains.get('dryRunMatches') || [];
3448
- const dryRunNetTools = matchedDomains.get('dryRunNetTools') || [];
3449
- const dryRunSearchString = matchedDomains.get('dryRunSearchString') || new Map();
3450
-
3451
- // Enhance matches with searchstring results
3452
- const enhancedMatches = dryRunMatches.map(match => {
3453
- const searchResult = dryRunSearchString.get(match.fullUrl);
3454
- return {
3455
- ...match,
3456
- searchStringMatch: searchResult && searchResult.matched ? searchResult : null,
3457
- searchStringChecked: match.needsSearchStringCheck
3458
- };
3459
- });
3460
-
3461
3345
  // Wait a moment for async nettools/searchstring operations to complete
3462
3346
  // Use fast timeout helper for Puppeteer 22.x compatibility
3463
3347
  await fastTimeout(TIMEOUTS.CURL_HANDLER_DELAY); // Wait for async operations
3464
3348
 
3465
- outputDryRunResults(currentUrl, enhancedMatches, dryRunNetTools, pageTitle);
3466
-
3467
- return { url: currentUrl, rules: [], success: true, dryRun: true, matchCount: dryRunMatches.length + dryRunNetTools.length };
3349
+ return { url: currentUrl, rules: [], success: true, dryRun: true, matchCount: dryRunResult.matchCount };
3468
3350
  } else {
3469
3351
  // Format rules using the output module
3470
3352
  const globalOptions = {
@@ -3884,14 +3766,9 @@ function setupFrameHandling(page, forceDebug) {
3884
3766
 
3885
3767
  // Handle dry run output file writing
3886
3768
  if (dryRunMode && outputFile && dryRunOutput.length > 0) {
3887
- try {
3888
- const dryRunContent = dryRunOutput.join('\n');
3889
- fs.writeFileSync(outputFile, dryRunContent);
3890
- if (!silentMode) {
3891
- console.log(`${messageColors.fileOp('📄 Dry run results saved to:')} ${outputFile}`);
3892
- }
3893
- } catch (writeErr) {
3894
- console.error(`❌ Failed to write dry run output to ${outputFile}: ${writeErr.message}`);
3769
+ const writeResult = writeDryRunOutput(outputFile, dryRunOutput, silentMode);
3770
+ if (!writeResult.success && forceDebug) {
3771
+ console.log(formatLogMessage('debug', `Dry run file write failed: ${writeResult.error}`));
3895
3772
  }
3896
3773
  }
3897
3774
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.21",
3
+ "version": "2.0.22",
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": {