@fanboynz/network-scanner 1.0.35
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/.github/workflows/npm-publish.yml +33 -0
- package/JSONMANUAL.md +121 -0
- package/LICENSE +674 -0
- package/README.md +357 -0
- package/config.json +74 -0
- package/lib/browserexit.js +522 -0
- package/lib/browserhealth.js +308 -0
- package/lib/cloudflare.js +660 -0
- package/lib/colorize.js +168 -0
- package/lib/compare.js +159 -0
- package/lib/compress.js +129 -0
- package/lib/fingerprint.js +613 -0
- package/lib/flowproxy.js +274 -0
- package/lib/grep.js +348 -0
- package/lib/ignore_similar.js +237 -0
- package/lib/nettools.js +1200 -0
- package/lib/output.js +633 -0
- package/lib/redirect.js +384 -0
- package/lib/searchstring.js +561 -0
- package/lib/validate_rules.js +1107 -0
- package/nwss.1 +824 -0
- package/nwss.js +2488 -0
- package/package.json +45 -0
- package/regex-samples.md +27 -0
- package/scanner-script-org.js +588 -0
package/lib/output.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadComparisonRules, filterUniqueRules } = require('./compare');
|
|
4
|
+
const { colorize, colors, messageColors, tags, formatLogMessage } = require('./colorize');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if domain matches any ignore patterns (supports wildcards)
|
|
8
|
+
* @param {string} domain - Domain to check
|
|
9
|
+
* @param {string[]} ignorePatterns - Array of ignore patterns
|
|
10
|
+
* @returns {boolean} True if domain should be ignored
|
|
11
|
+
*/
|
|
12
|
+
function matchesIgnoreDomain(domain, ignorePatterns) {
|
|
13
|
+
if (!ignorePatterns || !Array.isArray(ignorePatterns) || ignorePatterns.length === 0) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return ignorePatterns.some(pattern => {
|
|
18
|
+
if (pattern.includes('*')) {
|
|
19
|
+
// Convert wildcard pattern to regex
|
|
20
|
+
const regexPattern = pattern
|
|
21
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
22
|
+
.replace(/\*/g, '.*'); // Convert * to .*
|
|
23
|
+
return new RegExp(`^${regexPattern}$`).test(domain);
|
|
24
|
+
}
|
|
25
|
+
return domain.endsWith(pattern);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract domain from a formatted rule back to plain domain
|
|
31
|
+
* @param {string} rule - Formatted rule (e.g., "||domain.com^", "127.0.0.1 domain.com", etc.)
|
|
32
|
+
* @returns {string|null} Plain domain or null if cannot extract
|
|
33
|
+
*/
|
|
34
|
+
function extractDomainFromRule(rule) {
|
|
35
|
+
if (!rule || rule.startsWith('!')) {
|
|
36
|
+
return null; // Skip comments
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handle different output formats
|
|
40
|
+
if (rule.startsWith('||') && rule.includes('^')) {
|
|
41
|
+
// Adblock format: ||domain.com^ or ||domain.com^$script
|
|
42
|
+
return rule.substring(2).split('^')[0];
|
|
43
|
+
} else if (rule.match(/^(127\.0\.0\.1|0\.0\.0\.0)\s+/)) {
|
|
44
|
+
// Localhost format: 127.0.0.1 domain.com or 0.0.0.0 domain.com
|
|
45
|
+
return rule.split(/\s+/)[1];
|
|
46
|
+
} else if (rule.startsWith('local=/') && rule.endsWith('/')) {
|
|
47
|
+
// DNSmasq format: local=/domain.com/
|
|
48
|
+
return rule.substring(6, rule.length - 1);
|
|
49
|
+
} else if (rule.startsWith('server=/') && rule.endsWith('/')) {
|
|
50
|
+
// DNSmasq old format: server=/domain.com/
|
|
51
|
+
return rule.substring(7, rule.length - 1);
|
|
52
|
+
} else if (rule.startsWith('local-zone: "') && rule.includes('" always_null')) {
|
|
53
|
+
// Unbound format: local-zone: "domain.com." always_null
|
|
54
|
+
const domain = rule.substring(13).split('"')[0];
|
|
55
|
+
return domain.endsWith('.') ? domain.slice(0, -1) : domain;
|
|
56
|
+
} else if (rule.startsWith('{ +block } .')) {
|
|
57
|
+
// Privoxy format: { +block } .domain.com
|
|
58
|
+
return rule.substring(12);
|
|
59
|
+
} else if (rule.match(/^\(\^\|\\?\.\)/)) {
|
|
60
|
+
// Pi-hole regex format: (^|\.)domain\.com$
|
|
61
|
+
return rule.replace(/^\(\^\|\\?\.\)/, '').replace(/\\\./g, '.').replace(/\$$/, '');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If no format matches, assume it's already a plain domain
|
|
65
|
+
return rule.includes('.') ? rule : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Formats a domain according to the specified output mode
|
|
70
|
+
* @param {string} domain - The domain to format
|
|
71
|
+
* @param {object} options - Formatting options
|
|
72
|
+
* @param {boolean} options.localhost - Use 127.0.0.1 format
|
|
73
|
+
* @param {boolean} options.localhostAlt - Use 0.0.0.0 format
|
|
74
|
+
* @param {boolean} options.plain - Use plain domain format (no adblock syntax)
|
|
75
|
+
* @param {boolean} options.adblockRules - Generate adblock filter rules with resource types
|
|
76
|
+
* @param {boolean} options.dnsmasq - Use dnsmasq local format
|
|
77
|
+
* @param {boolean} options.dnsmasqOld - Use dnsmasq old server format
|
|
78
|
+
* @param {boolean} options.unbound - Use unbound local-zone format
|
|
79
|
+
* @param {boolean} options.privoxy - Use Privoxy block format
|
|
80
|
+
* @param {boolean} options.pihole - Use Pi-hole regex format
|
|
81
|
+
* @param {string} options.resourceType - Resource type for adblock rules (script, xhr, iframe, css, image, etc.)
|
|
82
|
+
* @returns {string} The formatted domain
|
|
83
|
+
*/
|
|
84
|
+
function formatDomain(domain, options = {}) {
|
|
85
|
+
const { localhost = false, localhostAlt = false, plain = false, adblockRules = false, dnsmasq = false, dnsmasqOld = false, unbound = false, privoxy = false, pihole = false, resourceType = null } = options;
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
// Validate domain length and format
|
|
89
|
+
if (!domain || domain.length <= 6 || !domain.includes('.')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If plain is true, always return just the domain regardless of other options
|
|
94
|
+
if (plain) {
|
|
95
|
+
return domain;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Apply specific format based on output mode
|
|
99
|
+
if (pihole) {
|
|
100
|
+
// Escape dots for regex and use Pi-hole format: (^|\.)domain\.com$
|
|
101
|
+
const escapedDomain = domain.replace(/\./g, '\\.');
|
|
102
|
+
return `(^|\\.)${escapedDomain}$`;
|
|
103
|
+
} else if (privoxy) {
|
|
104
|
+
return `{ +block } .${domain}`;
|
|
105
|
+
} else if (dnsmasq) {
|
|
106
|
+
return `local=/${domain}/`;
|
|
107
|
+
} else if (dnsmasqOld) {
|
|
108
|
+
return `server=/${domain}/`;
|
|
109
|
+
} else if (unbound) {
|
|
110
|
+
return `local-zone: "${domain}." always_null`;
|
|
111
|
+
} else if (localhost) {
|
|
112
|
+
return `127.0.0.1 ${domain}`;
|
|
113
|
+
} else if (localhostAlt) {
|
|
114
|
+
return `0.0.0.0 ${domain}`;
|
|
115
|
+
} else if (adblockRules && resourceType) {
|
|
116
|
+
// Generate adblock filter rules with resource type modifiers
|
|
117
|
+
return `||${domain}^${resourceType}`;
|
|
118
|
+
} else {
|
|
119
|
+
return `||${domain}^`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Maps Puppeteer resource types to adblock filter modifiers
|
|
125
|
+
* @param {string} resourceType - Puppeteer resource type
|
|
126
|
+
* @returns {string|null} Adblock filter modifier, or null if should be ignored
|
|
127
|
+
*/
|
|
128
|
+
function mapResourceTypeToAdblockModifier(resourceType) {
|
|
129
|
+
const typeMap = {
|
|
130
|
+
'script': 'script',
|
|
131
|
+
'xhr': 'xmlhttprequest',
|
|
132
|
+
'fetch': 'xmlhttprequest',
|
|
133
|
+
'stylesheet': 'stylesheet',
|
|
134
|
+
'image': 'image',
|
|
135
|
+
'font': 'font',
|
|
136
|
+
'document': 'document',
|
|
137
|
+
'subdocument': 'subdocument',
|
|
138
|
+
'iframe': 'subdocument',
|
|
139
|
+
'websocket': 'websocket',
|
|
140
|
+
'media': 'media',
|
|
141
|
+
'ping': 'ping',
|
|
142
|
+
'other': null // Ignore 'other' type - return null
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return typeMap[resourceType] || null; // Return null for unknown types too
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Formats an array of domains according to site and global settings
|
|
150
|
+
* @param {Set<string>|Map<string, Set<string>>} matchedDomains - Set of matched domains or Map of domain -> resource types
|
|
151
|
+
* @param {object} siteConfig - Site-specific configuration
|
|
152
|
+
* @param {object} globalOptions - Global formatting options
|
|
153
|
+
* @returns {string[]} Array of formatted rules
|
|
154
|
+
*/
|
|
155
|
+
function formatRules(matchedDomains, siteConfig = {}, globalOptions = {}) {
|
|
156
|
+
const {
|
|
157
|
+
localhostMode = false,
|
|
158
|
+
localhostModeAlt = false,
|
|
159
|
+
plainOutput = false,
|
|
160
|
+
adblockRulesMode = false,
|
|
161
|
+
dnsmasqMode = false,
|
|
162
|
+
dnsmasqOldMode = false,
|
|
163
|
+
unboundMode = false,
|
|
164
|
+
privoxyMode = false,
|
|
165
|
+
piholeMode = false
|
|
166
|
+
} = globalOptions;
|
|
167
|
+
|
|
168
|
+
// Site-level overrides
|
|
169
|
+
const siteLocalhost = siteConfig.localhost === true;
|
|
170
|
+
const siteLocalhostAlt = siteConfig.localhost_0_0_0_0 === true;
|
|
171
|
+
const sitePlainSetting = siteConfig.plain === true;
|
|
172
|
+
const siteAdblockRules = siteConfig.adblock_rules === true;
|
|
173
|
+
const siteDnsmasq = siteConfig.dnsmasq === true;
|
|
174
|
+
const siteDnsmasqOld = siteConfig.dnsmasq_old === true;
|
|
175
|
+
const siteUnbound = siteConfig.unbound === true;
|
|
176
|
+
const sitePrivoxy = siteConfig.privoxy === true;
|
|
177
|
+
const sitePihole = siteConfig.pihole === true;
|
|
178
|
+
|
|
179
|
+
// Validate output format compatibility - silently ignore incompatible combinations
|
|
180
|
+
const activeFormats = [
|
|
181
|
+
dnsmasqMode || siteDnsmasq,
|
|
182
|
+
dnsmasqOldMode || siteDnsmasqOld,
|
|
183
|
+
unboundMode || siteUnbound,
|
|
184
|
+
privoxyMode || sitePrivoxy,
|
|
185
|
+
piholeMode || sitePihole,
|
|
186
|
+
adblockRulesMode || siteAdblockRules,
|
|
187
|
+
localhostMode || siteLocalhost,
|
|
188
|
+
localhostModeAlt || siteLocalhostAlt,
|
|
189
|
+
plainOutput || sitePlainSetting
|
|
190
|
+
].filter(Boolean).length;
|
|
191
|
+
|
|
192
|
+
if (activeFormats > 1) {
|
|
193
|
+
// Multiple formats specified - fall back to standard adblock format
|
|
194
|
+
const formatOptions = {
|
|
195
|
+
localhost: false,
|
|
196
|
+
localhostAlt: false,
|
|
197
|
+
plain: false,
|
|
198
|
+
adblockRules: false,
|
|
199
|
+
dnsmasq: false,
|
|
200
|
+
dnsmasqOld: false,
|
|
201
|
+
unbound: false,
|
|
202
|
+
privoxy: false,
|
|
203
|
+
pihole: false
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const formattedRules = [];
|
|
207
|
+
const domainsToProcess = matchedDomains instanceof Set ? matchedDomains : new Set(matchedDomains.keys());
|
|
208
|
+
domainsToProcess.forEach(domain => {
|
|
209
|
+
const formatted = formatDomain(domain, formatOptions);
|
|
210
|
+
if (formatted) {
|
|
211
|
+
formattedRules.push(formatted);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return formattedRules;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Determine final formatting options
|
|
218
|
+
const formatOptions = {
|
|
219
|
+
localhost: localhostMode || siteLocalhost,
|
|
220
|
+
localhostAlt: localhostModeAlt || siteLocalhostAlt,
|
|
221
|
+
plain: plainOutput || sitePlainSetting,
|
|
222
|
+
adblockRules: adblockRulesMode || siteAdblockRules,
|
|
223
|
+
dnsmasq: dnsmasqMode || siteDnsmasq,
|
|
224
|
+
dnsmasqOld: dnsmasqOldMode || siteDnsmasqOld,
|
|
225
|
+
unbound: unboundMode || siteUnbound,
|
|
226
|
+
privoxy: privoxyMode || sitePrivoxy,
|
|
227
|
+
pihole: piholeMode || sitePihole
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const formattedRules = [];
|
|
231
|
+
|
|
232
|
+
if (matchedDomains instanceof Map && formatOptions.adblockRules) {
|
|
233
|
+
// Handle Map format with resource types for --adblock-rules
|
|
234
|
+
matchedDomains.forEach((resourceTypes, domain) => {
|
|
235
|
+
if (resourceTypes.size > 0) {
|
|
236
|
+
let hasValidResourceType = false;
|
|
237
|
+
|
|
238
|
+
// Generate one rule per resource type found for this domain
|
|
239
|
+
resourceTypes.forEach(resourceType => {
|
|
240
|
+
const adblockModifier = mapResourceTypeToAdblockModifier(resourceType);
|
|
241
|
+
// Skip if modifier is null (e.g., 'other' type)
|
|
242
|
+
if (adblockModifier) {
|
|
243
|
+
hasValidResourceType = true;
|
|
244
|
+
const formatted = formatDomain(domain, {
|
|
245
|
+
...formatOptions,
|
|
246
|
+
resourceType: adblockModifier
|
|
247
|
+
});
|
|
248
|
+
if (formatted) {
|
|
249
|
+
formattedRules.push(formatted);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// If no valid resource types were found, add a generic rule
|
|
255
|
+
if (!hasValidResourceType) {
|
|
256
|
+
const formatted = formatDomain(domain, formatOptions);
|
|
257
|
+
if (formatted) {
|
|
258
|
+
formattedRules.push(formatted);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
// Fallback to generic rule if no resource types
|
|
263
|
+
const formatted = formatDomain(domain, formatOptions);
|
|
264
|
+
if (formatted) {
|
|
265
|
+
formattedRules.push(formatted);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
} else {
|
|
270
|
+
// Handle Set format (legacy behavior) or other modes (including privoxy and pihole)
|
|
271
|
+
const domainsToProcess = matchedDomains instanceof Set ? matchedDomains : new Set(matchedDomains.keys());
|
|
272
|
+
domainsToProcess.forEach(domain => {
|
|
273
|
+
const formatted = formatDomain(domain, formatOptions);
|
|
274
|
+
if (formatted) {
|
|
275
|
+
formattedRules.push(formatted);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return formattedRules;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Removes duplicate rules while preserving comments (lines starting with !)
|
|
285
|
+
* @param {string[]} lines - Array of output lines
|
|
286
|
+
* @returns {string[]} Array with duplicates removed
|
|
287
|
+
*/
|
|
288
|
+
function removeDuplicates(lines) {
|
|
289
|
+
const uniqueLines = [];
|
|
290
|
+
const seenRules = new Set();
|
|
291
|
+
|
|
292
|
+
for (const line of lines) {
|
|
293
|
+
if (line.startsWith('!') || !seenRules.has(line)) {
|
|
294
|
+
uniqueLines.push(line);
|
|
295
|
+
if (!line.startsWith('!')) {
|
|
296
|
+
seenRules.add(line);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return uniqueLines;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Builds the final output lines from processing results
|
|
306
|
+
* @param {Array} results - Array of processing results from processUrl
|
|
307
|
+
* @param {object} options - Output options
|
|
308
|
+
* @param {boolean} options.showTitles - Include URL titles in output
|
|
309
|
+
* @param {boolean} options.removeDupes - Remove duplicate rules
|
|
310
|
+
* @param {string[]} options.ignoreDomains - Domains to filter out from final output
|
|
311
|
+
* @param {boolean} options.forLogFile - Include titles regardless of showTitles (for log files)
|
|
312
|
+
* @returns {object} Object containing outputLines and outputLinesWithTitles
|
|
313
|
+
*/
|
|
314
|
+
function buildOutputLines(results, options = {}) {
|
|
315
|
+
const { showTitles = false, removeDupes = false, ignoreDomains = [], forLogFile = false } = options;
|
|
316
|
+
|
|
317
|
+
// Filter and collect successful results with rules
|
|
318
|
+
const finalSiteRules = [];
|
|
319
|
+
let successfulPageLoads = 0;
|
|
320
|
+
|
|
321
|
+
results.forEach(result => {
|
|
322
|
+
if (result) {
|
|
323
|
+
if (result.success) {
|
|
324
|
+
successfulPageLoads++;
|
|
325
|
+
}
|
|
326
|
+
if (result.rules && result.rules.length > 0) {
|
|
327
|
+
finalSiteRules.push({ url: result.url, rules: result.rules });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Build output lines
|
|
333
|
+
const outputLines = [];
|
|
334
|
+
const outputLinesWithTitles = [];
|
|
335
|
+
let filteredOutCount = 0;
|
|
336
|
+
|
|
337
|
+
for (const { url, rules } of finalSiteRules) {
|
|
338
|
+
if (rules.length > 0) {
|
|
339
|
+
// Regular output (for -o files and console) - only add titles if --titles flag used
|
|
340
|
+
if (showTitles) {
|
|
341
|
+
outputLines.push(`! ${url}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Filter out ignored domains from rules
|
|
345
|
+
const filteredRules = rules.filter(rule => {
|
|
346
|
+
const domain = extractDomainFromRule(rule);
|
|
347
|
+
if (domain && matchesIgnoreDomain(domain, ignoreDomains)) {
|
|
348
|
+
filteredOutCount++;
|
|
349
|
+
|
|
350
|
+
// Log each filtered domain
|
|
351
|
+
if (options.forceDebug) {
|
|
352
|
+
console.log(formatLogMessage('debug', `[output-filter] Removed rule matching ignoreDomains: ${rule} (domain: ${domain})`));
|
|
353
|
+
} else if (!options.silentMode) {
|
|
354
|
+
console.log(formatLogMessage('info', `Filtered out: ${domain}`));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
return true;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
outputLines.push(...filteredRules);
|
|
363
|
+
|
|
364
|
+
// Output with titles (for auto-saved log files) - always add titles
|
|
365
|
+
outputLinesWithTitles.push(`! ${url}`);
|
|
366
|
+
outputLinesWithTitles.push(...filteredRules);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Log filtered domains if any were removed
|
|
371
|
+
if (filteredOutCount > 0) {
|
|
372
|
+
if (options.forceDebug) {
|
|
373
|
+
console.log(formatLogMessage('debug', `[output-filter] Total: ${filteredOutCount} rules filtered out matching ignoreDomains patterns`));
|
|
374
|
+
} else if (!options.silentMode) {
|
|
375
|
+
console.log(formatLogMessage('info', `${filteredOutCount} domains filtered out by ignoreDomains`));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Remove duplicates if requested
|
|
380
|
+
const finalOutputLines = removeDupes ? removeDuplicates(outputLines) : outputLines;
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
outputLines: finalOutputLines,
|
|
384
|
+
outputLinesWithTitles,
|
|
385
|
+
successfulPageLoads,
|
|
386
|
+
totalRules: finalOutputLines.filter(line => !line.startsWith('!')).length,
|
|
387
|
+
filteredOutCount
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Writes output to file or console
|
|
393
|
+
* @param {string[]} lines - Lines to output
|
|
394
|
+
* @param {string|null} outputFile - File path to write to, or null for console output
|
|
395
|
+
* @param {boolean} silentMode - Suppress console messages
|
|
396
|
+
* @returns {boolean} Success status
|
|
397
|
+
*/
|
|
398
|
+
function writeOutput(lines, outputFile = null, silentMode = false) {
|
|
399
|
+
try {
|
|
400
|
+
if (outputFile) {
|
|
401
|
+
// Ensure output directory exists
|
|
402
|
+
const outputDir = path.dirname(outputFile);
|
|
403
|
+
if (outputDir !== '.' && !fs.existsSync(outputDir)) {
|
|
404
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fs.writeFileSync(outputFile, lines.join('\n') + '\n');
|
|
408
|
+
if (!silentMode) {
|
|
409
|
+
console.log(`\n${messageColors.success('Rules saved to')} ${outputFile}`);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
// Console output
|
|
413
|
+
if (lines.length > 0 && !silentMode) {
|
|
414
|
+
console.log(`\n${messageColors.highlight('--- Generated Rules ---')}`);
|
|
415
|
+
}
|
|
416
|
+
console.log(lines.join('\n'));
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(`? Failed to write output: ${error.message}`);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Main output handler that combines all output operations
|
|
427
|
+
* @param {Array} results - Processing results from scanner
|
|
428
|
+
* @param {object} config - Output configuration
|
|
429
|
+
* @param {string[]} config.ignoreDomains - Domains to filter out from final output
|
|
430
|
+
* @returns {object} Output statistics and file paths
|
|
431
|
+
*/
|
|
432
|
+
function handleOutput(results, config = {}) {
|
|
433
|
+
const {
|
|
434
|
+
outputFile = null,
|
|
435
|
+
compareFile = null,
|
|
436
|
+
appendMode = false,
|
|
437
|
+
showTitles = false,
|
|
438
|
+
removeDupes = false,
|
|
439
|
+
silentMode = false,
|
|
440
|
+
dumpUrls = false,
|
|
441
|
+
adblockRulesLogFile = null,
|
|
442
|
+
forceDebug = false,
|
|
443
|
+
ignoreDomains = []
|
|
444
|
+
} = config;
|
|
445
|
+
|
|
446
|
+
// Handle append mode
|
|
447
|
+
if (outputFile && appendMode) {
|
|
448
|
+
try {
|
|
449
|
+
// Build output lines first
|
|
450
|
+
const {
|
|
451
|
+
outputLines,
|
|
452
|
+
outputLinesWithTitles,
|
|
453
|
+
successfulPageLoads,
|
|
454
|
+
totalRules,
|
|
455
|
+
filteredOutCount
|
|
456
|
+
} = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains: config.ignoreDomains, forceDebug: config.forceDebug });
|
|
457
|
+
|
|
458
|
+
// Apply remove-dupes to new results if requested (before comparing to existing file)
|
|
459
|
+
const deduplicatedOutputLines = removeDupes ? removeDuplicates(outputLines) : outputLines;
|
|
460
|
+
if (removeDupes && forceDebug) console.log(formatLogMessage('debug', `Applied --remove-dupes to new scan results before append comparison`));
|
|
461
|
+
|
|
462
|
+
// Read existing file content
|
|
463
|
+
let existingContent = '';
|
|
464
|
+
if (fs.existsSync(outputFile)) {
|
|
465
|
+
existingContent = fs.readFileSync(outputFile, 'utf8');
|
|
466
|
+
} else {
|
|
467
|
+
// File doesn't exist - append mode should create it
|
|
468
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Append mode: Creating new file ${outputFile}`));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Parse existing rules for comparison (exclude comments)
|
|
472
|
+
const existingRules = new Set();
|
|
473
|
+
if (existingContent.trim()) {
|
|
474
|
+
const lines = existingContent.trim().split('\n');
|
|
475
|
+
lines.forEach(line => {
|
|
476
|
+
const cleanLine = line.trim();
|
|
477
|
+
if (cleanLine && !cleanLine.startsWith('!') && !cleanLine.startsWith('#')) {
|
|
478
|
+
existingRules.add(cleanLine);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Filter out rules that already exist (exclude comments from filtering)
|
|
484
|
+
const newRules = deduplicatedOutputLines.filter(rule => {
|
|
485
|
+
return rule.startsWith('!') || !existingRules.has(rule);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
if (newRules.length > 0) {
|
|
489
|
+
// Prepare content to append
|
|
490
|
+
let appendContent = '';
|
|
491
|
+
|
|
492
|
+
// Ensure there's a newline before appending if file has content
|
|
493
|
+
if (existingContent && !existingContent.endsWith('\n')) {
|
|
494
|
+
appendContent = '\n';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Add new rules
|
|
498
|
+
appendContent += newRules.join('\n') + '\n';
|
|
499
|
+
|
|
500
|
+
// Append to file
|
|
501
|
+
fs.appendFileSync(outputFile, appendContent);
|
|
502
|
+
|
|
503
|
+
const newRuleCount = newRules.filter(rule => !rule.startsWith('!')).length;
|
|
504
|
+
if (!silentMode) {
|
|
505
|
+
console.log(`${messageColors.success('? Appended')} ${newRuleCount} new rules to: ${outputFile} (${existingRules.size} rules already existed${removeDupes ? ', duplicates removed' : ''})`);
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
if (!silentMode) {
|
|
509
|
+
const ruleCount = deduplicatedOutputLines.filter(rule => !rule.startsWith('!')).length;
|
|
510
|
+
console.log(`${messageColors.info('?')} No new rules to append - all ${ruleCount} rules already exist in: ${outputFile}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Write log file output if --dumpurls is enabled
|
|
515
|
+
let logSuccess = true;
|
|
516
|
+
if (dumpUrls && adblockRulesLogFile) {
|
|
517
|
+
logSuccess = writeOutput(outputLinesWithTitles, adblockRulesLogFile, silentMode);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const newRuleCount = newRules.filter(rule => !rule.startsWith('!')).length;
|
|
521
|
+
return {
|
|
522
|
+
success: logSuccess,
|
|
523
|
+
outputFile,
|
|
524
|
+
adblockRulesLogFile,
|
|
525
|
+
successfulPageLoads,
|
|
526
|
+
totalRules: newRuleCount,
|
|
527
|
+
filteredOutCount,
|
|
528
|
+
totalLines: newRules.length,
|
|
529
|
+
outputLines: null,
|
|
530
|
+
appendedRules: newRuleCount,
|
|
531
|
+
existingRules: existingRules.size
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
} catch (appendErr) {
|
|
535
|
+
console.error(`? Failed to append to ${outputFile}: ${appendErr.message}`);
|
|
536
|
+
return { success: false };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Build output lines
|
|
541
|
+
const {
|
|
542
|
+
outputLines,
|
|
543
|
+
outputLinesWithTitles,
|
|
544
|
+
successfulPageLoads,
|
|
545
|
+
totalRules,
|
|
546
|
+
filteredOutCount
|
|
547
|
+
} = buildOutputLines(results, { showTitles, removeDupes, ignoreDomains: config.ignoreDomains, forceDebug: config.forceDebug });
|
|
548
|
+
|
|
549
|
+
// Apply comparison filtering if compareFile is specified
|
|
550
|
+
let filteredOutputLines = outputLines;
|
|
551
|
+
if (compareFile && outputLines.length > 0) {
|
|
552
|
+
try {
|
|
553
|
+
const comparisonRules = loadComparisonRules(compareFile, config.forceDebug);
|
|
554
|
+
const originalCount = outputLines.filter(line => !line.startsWith('!')).length;
|
|
555
|
+
filteredOutputLines = filterUniqueRules(outputLines, comparisonRules, config.forceDebug);
|
|
556
|
+
|
|
557
|
+
if (!silentMode) {
|
|
558
|
+
console.log(formatLogMessage('compare', `Filtered ${originalCount - filteredOutputLines.filter(line => !line.startsWith('!')).length} existing rules, ${filteredOutputLines.filter(line => !line.startsWith('!')).length} unique rules remaining`));
|
|
559
|
+
|
|
560
|
+
}
|
|
561
|
+
} catch (compareError) {
|
|
562
|
+
console.error(messageColors.error('❌ Compare operation failed:') + ` ${compareError.message}`);
|
|
563
|
+
return { success: false, totalRules: 0, successfulPageLoads: 0 };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Write main output
|
|
568
|
+
const mainSuccess = writeOutput(filteredOutputLines, outputFile, silentMode);
|
|
569
|
+
|
|
570
|
+
// Write log file output if --dumpurls is enabled
|
|
571
|
+
let logSuccess = true;
|
|
572
|
+
if (dumpUrls && adblockRulesLogFile) {
|
|
573
|
+
logSuccess = writeOutput(outputLinesWithTitles, adblockRulesLogFile, silentMode);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
success: mainSuccess && logSuccess,
|
|
578
|
+
outputFile,
|
|
579
|
+
adblockRulesLogFile,
|
|
580
|
+
successfulPageLoads,
|
|
581
|
+
totalRules: filteredOutputLines.filter(line => !line.startsWith('!')).length,
|
|
582
|
+
filteredOutCount,
|
|
583
|
+
totalLines: filteredOutputLines.length,
|
|
584
|
+
outputLines: outputFile ? null : filteredOutputLines // Only return lines if not written to file
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Get output format description for debugging/logging
|
|
590
|
+
* @param {object} options - Format options
|
|
591
|
+
* @returns {string} Human-readable format description
|
|
592
|
+
*/
|
|
593
|
+
function getFormatDescription(options = {}) {
|
|
594
|
+
const { localhost = false, localhostAlt = false, plain = false, adblockRules = false, dnsmasq = false, dnsmasqOld = false, unbound = false, privoxy = false, pihole = false } = options;
|
|
595
|
+
|
|
596
|
+
// Plain always takes precedence
|
|
597
|
+
if (plain) {
|
|
598
|
+
return 'Plain domains only';
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (pihole) {
|
|
602
|
+
return 'Pi-hole regex format ((^|\\.)domain\\.com$)';
|
|
603
|
+
} else if (privoxy) {
|
|
604
|
+
return 'Privoxy format ({ +block } .domain.com)';
|
|
605
|
+
} else if (dnsmasq) {
|
|
606
|
+
return 'DNSmasq format (local=/domain.com/)';
|
|
607
|
+
} else if (dnsmasqOld) {
|
|
608
|
+
return 'DNSmasq old format (server=/domain.com/)';
|
|
609
|
+
} else if (unbound) {
|
|
610
|
+
return 'Unbound format (local-zone: "domain.com." always_null)';
|
|
611
|
+
} else if (adblockRules) {
|
|
612
|
+
return 'Adblock filter rules with resource type modifiers (||domain.com^$script)';
|
|
613
|
+
} else if (localhost) {
|
|
614
|
+
return 'Localhost format (127.0.0.1 domain.com)';
|
|
615
|
+
} else if (localhostAlt) {
|
|
616
|
+
return 'Localhost format (0.0.0.0 domain.com)';
|
|
617
|
+
} else {
|
|
618
|
+
return 'Adblock format (||domain.com^)';
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
module.exports = {
|
|
623
|
+
formatDomain,
|
|
624
|
+
formatRules,
|
|
625
|
+
removeDuplicates,
|
|
626
|
+
buildOutputLines,
|
|
627
|
+
writeOutput,
|
|
628
|
+
handleOutput,
|
|
629
|
+
getFormatDescription,
|
|
630
|
+
mapResourceTypeToAdblockModifier,
|
|
631
|
+
matchesIgnoreDomain,
|
|
632
|
+
extractDomainFromRule
|
|
633
|
+
};
|