@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/.github/workflows/npm-publish.yml +140 -11
- package/CHANGELOG.md +164 -0
- package/CLAUDE.md +40 -7
- package/README.md +29 -4
- package/lib/adblock-rust.js +23 -18
- package/lib/adblock.js +127 -82
- package/lib/browserexit.js +213 -203
- package/lib/browserhealth.js +85 -61
- package/lib/cdp.js +103 -81
- package/lib/clear_sitedata.js +61 -159
- package/lib/cloudflare.js +579 -409
- package/lib/colorize.js +29 -12
- package/lib/compare.js +16 -8
- package/lib/compress.js +2 -1
- package/lib/curl.js +287 -220
- package/lib/domain-cache.js +87 -40
- package/lib/dry-run.js +137 -194
- package/lib/fingerprint.js +341 -176
- package/lib/flowproxy.js +391 -188
- package/lib/ghost-cursor.js +8 -7
- package/lib/grep.js +248 -171
- package/lib/ignore_similar.js +70 -124
- package/lib/interaction.js +132 -235
- package/lib/nettools.js +309 -87
- package/lib/openvpn_vpn.js +12 -11
- package/lib/output.js +92 -59
- package/lib/post-processing.js +216 -162
- package/lib/proxy.js +9 -2
- package/lib/redirect.js +47 -31
- package/lib/referrer.js +158 -165
- package/lib/searchstring.js +290 -381
- package/lib/smart-cache.js +141 -91
- package/lib/socks-relay.js +21 -7
- package/lib/spawn-async.js +137 -0
- package/lib/validate_rules.js +188 -176
- package/lib/wireguard_vpn.js +111 -117
- package/nwss.js +743 -159
- package/package.json +4 -4
- package/scripts/test-stealth.js +281 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "
|
|
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": ">=
|
|
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.
|
|
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": ">=
|
|
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
|
+
});
|