@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.66",
3
+ "version": "3.0.1",
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": {
@@ -14,7 +14,7 @@
14
14
  "lru-cache": "^11.3.5",
15
15
  "p-limit": "^7.3.0",
16
16
  "psl": "^1.15.0",
17
- "puppeteer": ">=20.0.0",
17
+ "puppeteer": ">=24.0.0",
18
18
  "socks": "^2.8.9"
19
19
  },
20
20
  "overrides": {
@@ -37,7 +37,7 @@
37
37
  "author": "FanboyNZ",
38
38
  "license": "GPL-3.0",
39
39
  "engines": {
40
- "node": ">=22.0.0"
40
+ "node": ">=22.12.0"
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
@@ -52,7 +52,7 @@
52
52
  "homepage": "https://github.com/ryanbr/network-scanner",
53
53
  "optionalDependencies": {
54
54
  "adblock-rs": "^0.12.3",
55
- "puppeteer-core": ">=20.0.0"
55
+ "puppeteer-core": ">=24.0.0"
56
56
  },
57
57
  "devDependencies": {
58
58
  "eslint": "^10.0.2",
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Stealth integration smoke test.
4
+ *
5
+ * Launches Puppeteer, applies the project's full fingerprint spoofing stack
6
+ * (lib/fingerprint.js's applyAllFingerprintSpoofing), navigates to public
7
+ * bot-detection test pages, and reports what the page concluded about us.
8
+ *
9
+ * Purpose: replace "I think the spoof works" theoretical reviews with real
10
+ * signal -- which checks pass, which fail, which moved after a fingerprint
11
+ * change. Run before and after a stealth-related commit to A/B the impact.
12
+ *
13
+ * Usage:
14
+ * node scripts/test-stealth.js # all targets, human-readable
15
+ * node scripts/test-stealth.js sannysoft # one target
16
+ * node scripts/test-stealth.js --headful # show browser GUI
17
+ * node scripts/test-stealth.js --no-spoof # baseline (no fingerprint protection)
18
+ * node scripts/test-stealth.js --ua=firefox # change UA family
19
+ * node scripts/test-stealth.js --format=json # machine-readable output
20
+ * node scripts/test-stealth.js --help # show usage
21
+ *
22
+ * Environment:
23
+ * PUPPETEER_NO_SANDBOX=1 pass --no-sandbox --disable-setuid-sandbox to
24
+ * Chromium. Required when running as root (CI
25
+ * containers, some Docker setups). Off by default
26
+ * so local dev doesn't silently drop the sandbox.
27
+ *
28
+ * Targets (extend by adding to TARGETS below):
29
+ * sannysoft https://bot.sannysoft.com/ — classic fingerprint tests
30
+ * creepjs https://abrahamjuliot.github.io/creepjs/ — modern fingerprint suite
31
+ * browserleaks https://browserleaks.com/javascript — JS env probe
32
+ *
33
+ * Output: one line per target with PASS / WARN / FAIL counts (where parseable),
34
+ * plus a short summary of any explicit detection markers ("Bot detected",
35
+ * "Headless", etc.) found in the page text. With --format=json, emits a single
36
+ * JSON object suitable for piping to diff/jq for before/after comparison.
37
+ *
38
+ * This is a SMOKE test, not a unit test. It doesn't make assertions; it
39
+ * reports what the page reports. Use the output to decide if a stealth
40
+ * change moved the needle.
41
+ */
42
+
43
+ 'use strict';
44
+
45
+ const puppeteer = require('puppeteer');
46
+ const path = require('path');
47
+ const {
48
+ applyAllFingerprintSpoofing,
49
+ USER_AGENT_COLLECTIONS
50
+ } = require(path.resolve(__dirname, '..', 'lib', 'fingerprint'));
51
+
52
+ const args = process.argv.slice(2);
53
+ const HELP = args.includes('--help') || args.includes('-h');
54
+ const HEADFUL = args.includes('--headful');
55
+ const NO_SPOOF = args.includes('--no-spoof');
56
+ const UA_FLAG = (args.find(a => a.startsWith('--ua=')) || '').slice(5) || 'chrome';
57
+ const FORMAT = (args.find(a => a.startsWith('--format=')) || '').slice(9) || 'text';
58
+ const filterTargets = args.filter(a => !a.startsWith('-'));
59
+ // Anything starting with '-' is a flag claim; we validate the known set
60
+ // below so typos like "-headful" or "--no_spoof" don't silently no-op.
61
+ const flagArgs = args.filter(a => a.startsWith('-'));
62
+ const KNOWN_FLAGS = new Set(['--headful', '--no-spoof', '--help', '-h']);
63
+ const KNOWN_FLAG_PREFIXES = ['--ua=', '--format='];
64
+
65
+ const TARGETS = [
66
+ {
67
+ name: 'sannysoft',
68
+ url: 'https://bot.sannysoft.com/',
69
+ // Parse the result tables. Sannysoft uses td.passed / td.failed / td.warn.
70
+ extract: async (page) => {
71
+ return await page.evaluate(() => {
72
+ const cells = Array.from(document.querySelectorAll('td'));
73
+ const out = { passed: 0, failed: 0, warn: 0, total: 0, failures: [] };
74
+ for (const c of cells) {
75
+ const cls = c.className || '';
76
+ if (cls.includes('passed')) { out.passed++; out.total++; }
77
+ else if (cls.includes('failed')) {
78
+ out.failed++; out.total++;
79
+ // Try to capture the row label for context
80
+ const row = c.closest('tr');
81
+ const label = row?.querySelector('td')?.textContent?.trim() || '?';
82
+ out.failures.push(label);
83
+ }
84
+ else if (cls.includes('warn')) { out.warn++; out.total++; }
85
+ }
86
+ return out;
87
+ });
88
+ }
89
+ },
90
+ {
91
+ name: 'creepjs',
92
+ url: 'https://abrahamjuliot.github.io/creepjs/',
93
+ extract: async (page) => {
94
+ // CreepJS surfaces a trust score in the page. Wait briefly for the
95
+ // async fingerprinting tests to complete.
96
+ await page.waitForSelector('#fingerprint-data', { timeout: 30000 }).catch(() => {});
97
+ await new Promise(r => setTimeout(r, 8000)); // give async tests time
98
+ return await page.evaluate(() => {
99
+ const text = document.body.innerText || '';
100
+ // CreepJS reports a "Trust Score" percentage and individual signal entries.
101
+ const trustMatch = text.match(/Trust Score[:\s]+(\d+(?:\.\d+)?)\s*%/i);
102
+ const lieMatch = text.match(/lies[:\s]+(\d+)/i);
103
+ const botMatch = text.match(/bot[:\s]+(true|false)/i);
104
+ return {
105
+ trustScore: trustMatch ? parseFloat(trustMatch[1]) : null,
106
+ lies: lieMatch ? parseInt(lieMatch[1], 10) : null,
107
+ botDetected: botMatch ? botMatch[1] === 'true' : null,
108
+ excerpt: text.split('\n').slice(0, 15).join('\n').slice(0, 400)
109
+ };
110
+ });
111
+ }
112
+ },
113
+ {
114
+ name: 'browserleaks',
115
+ url: 'https://browserleaks.com/javascript',
116
+ extract: async (page) => {
117
+ return await page.evaluate(() => {
118
+ // browserleaks shows the values; we just capture the navigator-related ones
119
+ // and report which look anomalous.
120
+ return {
121
+ userAgent: navigator.userAgent,
122
+ platform: navigator.platform,
123
+ webdriver: navigator.webdriver,
124
+ languages: JSON.stringify(navigator.languages),
125
+ hardwareConcurrency: navigator.hardwareConcurrency,
126
+ deviceMemory: navigator.deviceMemory,
127
+ plugins: navigator.plugins?.length,
128
+ chromeRuntime: typeof window.chrome?.runtime,
129
+ chromeRuntimeVersion: (() => { try { return window.chrome?.runtime?.getManifest?.()?.version; } catch (e) { return 'error'; } })(),
130
+ windowChromeDescriptor: (() => {
131
+ const d = Object.getOwnPropertyDescriptor(window, 'chrome');
132
+ return d ? `writable=${d.writable},enumerable=${d.enumerable},configurable=${d.configurable}` : 'no-descriptor';
133
+ })(),
134
+ errorName: Error.name,
135
+ errorLength: Error.length,
136
+ rtcName: window.RTCPeerConnection?.name,
137
+ imageName: window.Image?.name
138
+ };
139
+ });
140
+ }
141
+ }
142
+ ];
143
+
144
+ function printHelp() {
145
+ console.log(`Usage: node scripts/test-stealth.js [options] [target...]
146
+
147
+ Options:
148
+ --headful launch with browser GUI visible
149
+ --no-spoof baseline run — skip applyAllFingerprintSpoofing
150
+ --ua=<family> UA family to spoof (default: chrome)
151
+ valid: ${Array.from(USER_AGENT_COLLECTIONS.keys()).join(', ')}
152
+ --format=<fmt> output format: text (default) | json
153
+ --help, -h show this message
154
+
155
+ Environment:
156
+ PUPPETEER_NO_SANDBOX=1 pass --no-sandbox to Chromium (required in some CI)
157
+
158
+ Targets: ${TARGETS.map(t => t.name).join(', ')} (default: all)`);
159
+ }
160
+
161
+ function formatResult(target, result) {
162
+ const lines = [`\n=== ${target.name} (${target.url}) ===`];
163
+ if (target.name === 'sannysoft') {
164
+ lines.push(` passed: ${result.passed} | warn: ${result.warn} | failed: ${result.failed} | total: ${result.total}`);
165
+ if (result.failures.length) {
166
+ lines.push(` failure rows: ${result.failures.slice(0, 10).join(', ')}${result.failures.length > 10 ? ` ... +${result.failures.length - 10} more` : ''}`);
167
+ }
168
+ } else if (target.name === 'creepjs') {
169
+ lines.push(` trust score: ${result.trustScore ?? 'n/a'}%`);
170
+ lines.push(` lies detected: ${result.lies ?? 'n/a'}`);
171
+ lines.push(` bot flagged: ${result.botDetected ?? 'n/a'}`);
172
+ if (result.excerpt) lines.push(` excerpt:\n ${result.excerpt.split('\n').join('\n ')}`);
173
+ } else if (target.name === 'browserleaks') {
174
+ for (const [k, v] of Object.entries(result)) {
175
+ lines.push(` ${k.padEnd(24)} ${v}`);
176
+ }
177
+ }
178
+ return lines.join('\n');
179
+ }
180
+
181
+ (async () => {
182
+ if (HELP) { printHelp(); process.exit(0); }
183
+
184
+ // Validate --ua= against the canonical UA list. Previously a typo like
185
+ // --ua=opera silently fell through to applyUserAgentSpoofing's "unknown UA,
186
+ // no-op" path, producing run results that looked spoofed but weren't.
187
+ if (!USER_AGENT_COLLECTIONS.has(UA_FLAG)) {
188
+ console.error(`Invalid --ua=${UA_FLAG}. Valid: ${Array.from(USER_AGENT_COLLECTIONS.keys()).join(', ')}`);
189
+ process.exit(2);
190
+ }
191
+
192
+ if (!['text', 'json'].includes(FORMAT)) {
193
+ console.error(`Invalid --format=${FORMAT}. Valid: text, json`);
194
+ process.exit(2);
195
+ }
196
+
197
+ // Reject unrecognised flags before we launch a browser. Typos like
198
+ // "-headful" or "--no_spoof" used to silently no-op and produce a
199
+ // misleading "spoof on" run that wasn't actually spoofed.
200
+ const badFlags = flagArgs.filter(f =>
201
+ !KNOWN_FLAGS.has(f) && !KNOWN_FLAG_PREFIXES.some(p => f.startsWith(p))
202
+ );
203
+ if (badFlags.length) {
204
+ console.error(`Unrecognised flag(s): ${badFlags.join(', ')}. See --help.`);
205
+ process.exit(2);
206
+ }
207
+
208
+ const targetsToRun = filterTargets.length
209
+ ? TARGETS.filter(t => filterTargets.includes(t.name))
210
+ : TARGETS;
211
+
212
+ if (targetsToRun.length === 0) {
213
+ console.error(`No targets matched. Available: ${TARGETS.map(t => t.name).join(', ')}`);
214
+ process.exit(2);
215
+ }
216
+
217
+ if (FORMAT === 'text') {
218
+ console.log(`Stealth test config: spoof=${!NO_SPOOF}, ua=${UA_FLAG}, headful=${HEADFUL}`);
219
+ console.log(`Targets: ${targetsToRun.map(t => t.name).join(', ')}`);
220
+ }
221
+
222
+ // Sandbox is on by default; opt out via env var rather than baking
223
+ // --no-sandbox into the launch line. CI-as-root needs it; local dev should
224
+ // not silently drop the sandbox just because the test happens to start it.
225
+ const launchArgs = ['--disable-blink-features=AutomationControlled'];
226
+ if (process.env.PUPPETEER_NO_SANDBOX === '1') {
227
+ launchArgs.push('--no-sandbox', '--disable-setuid-sandbox');
228
+ }
229
+
230
+ const browser = await puppeteer.launch({
231
+ headless: !HEADFUL,
232
+ args: launchArgs
233
+ });
234
+
235
+ // Collected for JSON output (and to support a future --fail-on-detection
236
+ // exit code without restructuring the loop).
237
+ const collected = [];
238
+
239
+ try {
240
+ for (const target of targetsToRun) {
241
+ const page = await browser.newPage();
242
+ const started = Date.now();
243
+ try {
244
+ if (!NO_SPOOF) {
245
+ // Apply the same spoofing stack nwss.js uses for real scans.
246
+ await applyAllFingerprintSpoofing(page,
247
+ { userAgent: UA_FLAG, fingerprint_protection: 'random' },
248
+ false,
249
+ target.url
250
+ );
251
+ }
252
+ await page.goto(target.url, { waitUntil: 'networkidle2', timeout: 60000 });
253
+ const result = await target.extract(page);
254
+ collected.push({ name: target.name, url: target.url, ok: true, durationMs: Date.now() - started, result });
255
+ if (FORMAT === 'text') console.log(formatResult(target, result));
256
+ } catch (err) {
257
+ collected.push({ name: target.name, url: target.url, ok: false, durationMs: Date.now() - started, error: err.message });
258
+ if (FORMAT === 'text') {
259
+ console.error(`\n=== ${target.name} (${target.url}) ===`);
260
+ console.error(` ERROR: ${err.message}`);
261
+ }
262
+ } finally {
263
+ await page.close().catch(() => {});
264
+ }
265
+ }
266
+ } finally {
267
+ await browser.close().catch(() => {});
268
+ }
269
+
270
+ if (FORMAT === 'json') {
271
+ // Single object, not NDJSON — easier to diff with `jq` or `diff` between
272
+ // before/after runs. Schema is stable: top-level config + targets[].
273
+ process.stdout.write(JSON.stringify({
274
+ config: { spoof: !NO_SPOOF, ua: UA_FLAG, headful: HEADFUL, noSandbox: process.env.PUPPETEER_NO_SANDBOX === '1' },
275
+ targets: collected
276
+ }, null, 2) + '\n');
277
+ }
278
+ })().catch(err => {
279
+ console.error('test-stealth fatal:', err);
280
+ process.exit(1);
281
+ });