@fanboynz/network-scanner 2.0.66 → 3.0.1

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/lib/dry-run.js CHANGED
@@ -4,11 +4,13 @@
4
4
  const fs = require('fs');
5
5
  const { messageColors, formatLogMessage } = require('./colorize');
6
6
 
7
- // Constants for dry run collection keys
7
+ // Constants for dry run collection keys. SEARCH_STRING was removed —
8
+ // addDryRunSearchString had zero callers, so the map was never populated
9
+ // and the downstream "Searchstring Match" enhancement always produced
10
+ // null. See the cleanup comment in processDryRunResults.
8
11
  const DRY_RUN_KEYS = {
9
12
  MATCHES: 'dryRunMatches',
10
- NET_TOOLS: 'dryRunNetTools',
11
- SEARCH_STRING: 'dryRunSearchString'
13
+ NET_TOOLS: 'dryRunNetTools'
12
14
  };
13
15
 
14
16
  /**
@@ -23,7 +25,6 @@ function initializeDryRunCollections(matchedDomains) {
23
25
 
24
26
  matchedDomains.set(DRY_RUN_KEYS.MATCHES, []);
25
27
  matchedDomains.set(DRY_RUN_KEYS.NET_TOOLS, []);
26
- matchedDomains.set(DRY_RUN_KEYS.SEARCH_STRING, new Map());
27
28
  }
28
29
 
29
30
  /**
@@ -35,10 +36,15 @@ function validateMatchData(matchData) {
35
36
  if (!matchData || typeof matchData !== 'object') {
36
37
  throw new Error('Match data must be an object');
37
38
  }
38
-
39
+
39
40
  const requiredFields = ['regex', 'domain', 'resourceType', 'fullUrl'];
40
41
  for (const field of requiredFields) {
41
- if (!(field in matchData)) {
42
+ // Check VALUE, not just key presence. The old `field in matchData`
43
+ // accepted `{regex: undefined, ...}` because `in` only tests for
44
+ // the property's existence on the object. The downstream output
45
+ // then printed 'unknown' via `item.regex || 'unknown'` defensive
46
+ // fallbacks — validation that doesn't catch this defeats its purpose.
47
+ if (matchData[field] === undefined || matchData[field] === null) {
42
48
  throw new Error(`Match data missing required field: ${field}`);
43
49
  }
44
50
  }
@@ -53,10 +59,11 @@ function validateNetToolsData(netToolsData) {
53
59
  if (!netToolsData || typeof netToolsData !== 'object') {
54
60
  throw new Error('NetTools data must be an object');
55
61
  }
56
-
62
+
57
63
  const requiredFields = ['domain', 'tool', 'matchType', 'matchedTerm'];
58
64
  for (const field of requiredFields) {
59
- if (!(field in netToolsData)) {
65
+ // Value check (see validateMatchData for the rationale).
66
+ if (netToolsData[field] === undefined || netToolsData[field] === null) {
60
67
  throw new Error(`NetTools data missing required field: ${field}`);
61
68
  }
62
69
  }
@@ -108,36 +115,6 @@ function addDryRunNetTools(matchedDomains, netToolsData) {
108
115
  });
109
116
  }
110
117
 
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
118
  /**
142
119
  * Safely truncate long URLs for display
143
120
  * @param {string} url - URL to truncate
@@ -151,19 +128,6 @@ function truncateUrl(url, maxLength = 80) {
151
128
  return url.substring(0, maxLength - 3) + '...';
152
129
  }
153
130
 
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
131
  /**
168
132
  * Generate adblock rule from domain and resource type
169
133
  * @param {string} domain - Domain name
@@ -192,112 +156,130 @@ function generateAdblockRule(domain, resourceType = null) {
192
156
  function outputDryRunResults(url, matchedItems = [], netToolsResults = [], pageTitle = '', outputFile = null, dryRunOutput = []) {
193
157
  try {
194
158
  const lines = [];
159
+
160
+ // emit() — single source of truth for output. Writes the plain
161
+ // version to the file-output array AND the (possibly colored)
162
+ // version to the console. Previously every output line was a
163
+ // paired lines.push(...) + console.log(...) statement, often in
164
+ // separate blocks (file pushes first, then console logs), so
165
+ // drift between file and terminal output was a real risk every
166
+ // time someone edited only one half of a pair.
167
+ const emit = (plain, colored = plain) => {
168
+ lines.push(plain);
169
+ console.log(colored);
170
+ };
171
+
195
172
  const truncatedUrl = truncateUrl(url);
196
-
197
- lines.push(`\n=== DRY RUN RESULTS === ${truncatedUrl}`);
198
- console.log(`\n${messageColors.scanning('=== DRY RUN RESULTS ===')} ${truncatedUrl}`);
199
-
173
+
174
+ emit(
175
+ `\n=== DRY RUN RESULTS === ${truncatedUrl}`,
176
+ `\n${messageColors.scanning('=== DRY RUN RESULTS ===')} ${truncatedUrl}`
177
+ );
178
+
200
179
  if (pageTitle && pageTitle.trim()) {
201
180
  const cleanTitle = pageTitle.trim().substring(0, 200); // Limit title length
202
- lines.push(`Title: ${cleanTitle}`);
203
- console.log(`${messageColors.info('Title:')} ${cleanTitle}`);
181
+ emit(
182
+ `Title: ${cleanTitle}`,
183
+ `${messageColors.info('Title:')} ${cleanTitle}`
184
+ );
204
185
  }
205
-
186
+
206
187
  const totalMatches = matchedItems.length + netToolsResults.length;
207
-
188
+
208
189
  if (totalMatches === 0) {
209
190
  const noMatchMsg = `No matching rules found on ${truncatedUrl}`;
210
- lines.push(noMatchMsg);
211
-
191
+ emit(noMatchMsg, messageColors.warn(noMatchMsg));
192
+
212
193
  if (outputFile) {
213
194
  dryRunOutput.push(...lines);
214
195
  dryRunOutput.push(''); // Add empty line
215
196
  }
216
- console.log(messageColors.warn(noMatchMsg));
217
197
  return;
218
198
  }
219
-
220
- lines.push(`Matches found: ${totalMatches}`);
221
- console.log(`${messageColors.success('Matches found:')} ${totalMatches}`);
222
-
199
+
200
+ emit(
201
+ `Matches found: ${totalMatches}`,
202
+ `${messageColors.success('Matches found:')} ${totalMatches}`
203
+ );
204
+
223
205
  // Process regex matches
224
206
  matchedItems.forEach((item, index) => {
225
207
  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
208
+ emit(''); // blank separator before each match item
209
+ emit(
210
+ `[${index + 1}] Regex Match:`,
211
+ `${messageColors.highlight(`[${index + 1}]`)} ${messageColors.match('Regex Match:')}`
212
+ );
213
+ emit(` Pattern: ${item.regex || 'unknown'}`);
214
+ emit(` Domain: ${item.domain || 'unknown'}`);
215
+ emit(` Resource Type: ${item.resourceType || 'unknown'}`);
216
+ emit(` Full URL: ${truncateUrl(item.fullUrl || '')}`);
217
+
240
218
  if (item.wasBlocked) {
241
- lines.push(` Status: BLOCKED (even_blocked enabled)`);
242
- console.log(` ${messageColors.warn('Status:')} BLOCKED (even_blocked enabled)`);
219
+ emit(
220
+ ` Status: BLOCKED (even_blocked enabled)`,
221
+ ` ${messageColors.warn('Status:')} BLOCKED (even_blocked enabled)`
222
+ );
243
223
  }
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`);
224
+
225
+ // Searchstring "not found" see processDryRunResults comment
226
+ // for why the positive-match branch was removed.
227
+ if (item.searchStringChecked) {
228
+ emit(
229
+ ` Searchstring: No matches found in content`,
230
+ ` ${messageColors.warn('✗ Searchstring:')} No matches found in content`
231
+ );
253
232
  }
254
-
255
- // Generate adblock rule
233
+
256
234
  const adblockRule = generateAdblockRule(item.domain, item.resourceType);
257
- lines.push(` Adblock Rule: ${adblockRule}`);
258
- console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
259
-
235
+ emit(
236
+ ` Adblock Rule: ${adblockRule}`,
237
+ ` ${messageColors.info('Adblock Rule:')} ${adblockRule}`
238
+ );
239
+
260
240
  } catch (itemErr) {
261
241
  const errorMsg = `Error processing match item ${index + 1}: ${itemErr.message}`;
262
- lines.push(` Error: ${errorMsg}`);
263
- console.log(` ${messageColors.warn('Error:')} ${errorMsg}`);
242
+ emit(
243
+ ` Error: ${errorMsg}`,
244
+ ` ${messageColors.warn('Error:')} ${errorMsg}`
245
+ );
264
246
  }
265
247
  });
266
-
267
- // Process nettools results
248
+
249
+ // Process nettools results
268
250
  netToolsResults.forEach((result, index) => {
269
251
  try {
270
252
  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
-
253
+ emit(''); // blank separator before each nettools item
254
+ emit(
255
+ `[${resultIndex}] NetTools Match:`,
256
+ `${messageColors.highlight(`[${resultIndex}]`)} ${messageColors.match('NetTools Match:')}`
257
+ );
258
+ emit(` Domain: ${result.domain || 'unknown'}`);
259
+ emit(` Tool: ${(result.tool || 'unknown').toUpperCase()}`);
260
+
276
261
  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
-
262
+ emit(
263
+ ` ✓ Match: ${matchDesc}`,
264
+ ` ${messageColors.success('✓ Match:')} ${matchDesc}`
265
+ );
266
+
288
267
  if (result.details) {
289
- console.log(` Details: ${result.details}`);
268
+ emit(` Details: ${result.details}`);
290
269
  }
291
-
292
- // Generate adblock rule for nettools matches
270
+
293
271
  const adblockRule = generateAdblockRule(result.domain);
294
- lines.push(` Adblock Rule: ${adblockRule}`);
295
- console.log(` ${messageColors.info('Adblock Rule:')} ${adblockRule}`);
296
-
272
+ emit(
273
+ ` Adblock Rule: ${adblockRule}`,
274
+ ` ${messageColors.info('Adblock Rule:')} ${adblockRule}`
275
+ );
276
+
297
277
  } catch (resultErr) {
298
278
  const errorMsg = `Error processing nettools result ${index + 1}: ${resultErr.message}`;
299
- lines.push(` Error: ${errorMsg}`);
300
- console.log(` ${messageColors.warn('Error:')} ${errorMsg}`);
279
+ emit(
280
+ ` Error: ${errorMsg}`,
281
+ ` ${messageColors.warn('Error:')} ${errorMsg}`
282
+ );
301
283
  }
302
284
  });
303
285
 
@@ -306,7 +288,7 @@ function outputDryRunResults(url, matchedItems = [], netToolsResults = [], pageT
306
288
  dryRunOutput.push(...lines);
307
289
  dryRunOutput.push(''); // Add empty line between sites
308
290
  }
309
-
291
+
310
292
  } catch (outputErr) {
311
293
  const errorMsg = `Error in outputDryRunResults: ${outputErr.message}`;
312
294
  console.error(messageColors.error(errorMsg));
@@ -347,34 +329,27 @@ async function processDryRunResults(currentUrl, matchedDomains, page, outputFile
347
329
  if (forceDebug) {
348
330
  console.log(formatLogMessage('debug', `Failed to get page title for ${currentUrl}: ${titleErr.message}`));
349
331
  }
350
- pageTitle = 'Title unavailable';
332
+ // Leave pageTitle as '' (its initial value) on failure — the
333
+ // truthy check in outputDryRunResults then skips the Title line
334
+ // entirely. Previously we set 'Title unavailable' here, which
335
+ // was truthy and got printed as if it were the page's real
336
+ // title: 'Title: Title unavailable'.
351
337
  }
352
338
 
353
339
  // Get collected matches with safe fallbacks
354
340
  const dryRunMatches = matchedDomains.get(DRY_RUN_KEYS.MATCHES) || [];
355
341
  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
- });
342
+
343
+ // Enhance matches with the searchstring-checked flag from the
344
+ // incoming match data. Previously this also looked up positive
345
+ // searchstring results in a `dryRunSearchString` Map — but
346
+ // `addDryRunSearchString` was never wired to any caller, so the
347
+ // map was always empty and `searchStringMatch` was always null.
348
+ // Removed that dead lookup and the per-item try/catch it required.
349
+ const enhancedMatches = dryRunMatches.map((match) => ({
350
+ ...match,
351
+ searchStringChecked: Boolean(match.needsSearchStringCheck)
352
+ }));
378
353
 
379
354
  outputDryRunResults(currentUrl, enhancedMatches, dryRunNetTools, pageTitle, outputFile, dryRunOutput);
380
355
 
@@ -443,7 +418,7 @@ function writeDryRunOutput(outputFile, dryRunOutput, silentMode = false) {
443
418
  fs.writeFileSync(outputFile, dryRunContent);
444
419
 
445
420
  if (!silentMode) {
446
- console.log(`${messageColors.fileOp('?? Dry run results saved to:')} ${outputFile}`);
421
+ console.log(`${messageColors.fileOp('📄 Dry run results saved to:')} ${outputFile}`);
447
422
  }
448
423
 
449
424
  return {
@@ -456,60 +431,28 @@ function writeDryRunOutput(outputFile, dryRunOutput, silentMode = false) {
456
431
 
457
432
  } catch (writeErr) {
458
433
  const errorMsg = `Failed to write dry run output to ${outputFile}: ${writeErr.message}`;
459
- console.error(`? ${errorMsg}`);
460
-
461
- return {
462
- success: false,
434
+ // Matches outputDryRunResults / processDryRunResults error format —
435
+ // was bare `console.error('✗ ${errorMsg}')` here, the odd one out
436
+ // among the three error paths in this module.
437
+ console.error(messageColors.error(errorMsg));
438
+
439
+ return {
440
+ success: false,
463
441
  error: errorMsg,
464
442
  written: false
465
443
  };
466
444
  }
467
445
  }
468
446
 
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
-
447
+ // Public surface used by nwss.js. Internal helpers (truncateUrl,
448
+ // generateAdblockRule, validateMatchData, validateNetToolsData,
449
+ // outputDryRunResults) stay module-private. DRY_RUN_KEYS, getDryRunStats,
450
+ // addDryRunSearchString, and formatSearchStringMatch were removed —
451
+ // see comments at their original sites for details.
495
452
  module.exports = {
496
- // Constants
497
- DRY_RUN_KEYS,
498
-
499
- // Core functions
500
453
  initializeDryRunCollections,
501
454
  addDryRunMatch,
502
455
  addDryRunNetTools,
503
- addDryRunSearchString,
504
456
  processDryRunResults,
505
- writeDryRunOutput,
506
-
507
- // Utility functions
508
- getDryRunStats,
509
- validateMatchData,
510
- validateNetToolsData,
511
- truncateUrl,
512
- formatSearchStringMatch,
513
- generateAdblockRule,
514
- outputDryRunResults
457
+ writeDryRunOutput
515
458
  };