@fanboynz/network-scanner 2.0.53 → 2.0.55
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/browserexit.js +3 -1
- package/lib/fingerprint.js +102 -35
- package/lib/interaction.js +121 -5
- package/lib/nettools.js +68 -61
- package/nwss.js +28 -7
- package/package.json +1 -1
package/lib/browserexit.js
CHANGED
|
@@ -15,7 +15,7 @@ const CHROME_TEMP_PATHS = [
|
|
|
15
15
|
];
|
|
16
16
|
|
|
17
17
|
const CHROME_TEMP_PATTERNS = [
|
|
18
|
-
'
|
|
18
|
+
'com.google.Chrome.*', // Google Chrome temp files (no leading dot)
|
|
19
19
|
'.org.chromium.Chromium.*',
|
|
20
20
|
'puppeteer-*'
|
|
21
21
|
];
|
|
@@ -39,6 +39,7 @@ async function cleanupChromeTempFiles(options = {}) {
|
|
|
39
39
|
|
|
40
40
|
// Base cleanup commands for standard temp directories
|
|
41
41
|
const cleanupCommands = [
|
|
42
|
+
'rm -rf /tmp/com.google.Chrome.* 2>/dev/null || true',
|
|
42
43
|
'rm -rf /tmp/.com.google.Chrome.* 2>/dev/null || true',
|
|
43
44
|
'rm -rf /tmp/.org.chromium.Chromium.* 2>/dev/null || true',
|
|
44
45
|
'rm -rf /tmp/puppeteer-* 2>/dev/null || true',
|
|
@@ -48,6 +49,7 @@ async function cleanupChromeTempFiles(options = {}) {
|
|
|
48
49
|
|
|
49
50
|
// Add snap-specific cleanup if requested
|
|
50
51
|
if (includeSnapTemp || comprehensive) {
|
|
52
|
+
cleanupCommands.push('rm -rf /dev/shm/com.google.Chrome.* 2>/dev/null || true');
|
|
51
53
|
cleanupCommands.push(
|
|
52
54
|
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/.org.chromium.Chromium.* 2>/dev/null || true',
|
|
53
55
|
'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/puppeteer-* 2>/dev/null || true'
|
package/lib/fingerprint.js
CHANGED
|
@@ -24,28 +24,6 @@ function seededRandom(seed) {
|
|
|
24
24
|
const _fingerprintCache = new Map();
|
|
25
25
|
|
|
26
26
|
// Type-specific property spoofing functions for monomorphic optimization
|
|
27
|
-
function spoofNavigatorProperties(navigator, properties, options = {}) {
|
|
28
|
-
if (!navigator || typeof navigator !== 'object') return false;
|
|
29
|
-
|
|
30
|
-
for (const [prop, descriptor] of Object.entries(properties)) {
|
|
31
|
-
if (!safeDefineProperty(navigator, prop, descriptor, options)) {
|
|
32
|
-
if (options.debug) console.log(`[fingerprint] Failed to spoof navigator.${prop}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function spoofScreenProperties(screen, properties, options = {}) {
|
|
39
|
-
if (!screen || typeof screen !== 'object') return false;
|
|
40
|
-
|
|
41
|
-
for (const [prop, descriptor] of Object.entries(properties)) {
|
|
42
|
-
if (!safeDefineProperty(screen, prop, descriptor, options)) {
|
|
43
|
-
if (options.debug) console.log(`[fingerprint] Failed to spoof screen.${prop}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
27
|
// Built-in properties that should not be modified
|
|
50
28
|
const BUILT_IN_PROPERTIES = new Set([
|
|
51
29
|
'href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash',
|
|
@@ -379,6 +357,27 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
379
357
|
return false;
|
|
380
358
|
};
|
|
381
359
|
})();
|
|
360
|
+
|
|
361
|
+
// Function.prototype.toString protection — make spoofed functions appear native
|
|
362
|
+
// Must be installed BEFORE any property overrides so all spoofs are protected
|
|
363
|
+
const nativeFunctionStore = new WeakMap();
|
|
364
|
+
const originalToString = Function.prototype.toString;
|
|
365
|
+
|
|
366
|
+
function maskAsNative(fn, nativeName) {
|
|
367
|
+
if (typeof fn === 'function') {
|
|
368
|
+
nativeFunctionStore.set(fn, nativeName || fn.name || '');
|
|
369
|
+
}
|
|
370
|
+
return fn;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Function.prototype.toString = function() {
|
|
374
|
+
if (nativeFunctionStore.has(this)) {
|
|
375
|
+
return `function ${nativeFunctionStore.get(this)}() { [native code] }`;
|
|
376
|
+
}
|
|
377
|
+
return originalToString.call(this);
|
|
378
|
+
};
|
|
379
|
+
// Protect the toString override itself
|
|
380
|
+
nativeFunctionStore.set(Function.prototype.toString, 'toString');
|
|
382
381
|
|
|
383
382
|
// Create safe property definition helper
|
|
384
383
|
function safeDefinePropertyLocal(target, property, descriptor) {
|
|
@@ -439,10 +438,14 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
439
438
|
// Remove webdriver properties
|
|
440
439
|
//
|
|
441
440
|
safeExecute(() => {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
} catch (e) {}
|
|
445
|
-
|
|
441
|
+
// In real Chrome, webdriver lives on Navigator.prototype, not the instance.
|
|
442
|
+
// Override it there so Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns undefined.
|
|
443
|
+
try { delete navigator.webdriver; } catch (e) {}
|
|
444
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
445
|
+
get: () => false,
|
|
446
|
+
configurable: true,
|
|
447
|
+
enumerable: true
|
|
448
|
+
});
|
|
446
449
|
}, 'webdriver removal');
|
|
447
450
|
|
|
448
451
|
// Remove automation properties
|
|
@@ -861,6 +864,8 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
861
864
|
if (typeof originalStack === 'string') {
|
|
862
865
|
return originalStack
|
|
863
866
|
.replace(/.*puppeteer.*\n?/gi, '')
|
|
867
|
+
.replace(/.*__puppeteer_evaluation_script__.*\n?/gi, '')
|
|
868
|
+
.replace(/.*evaluateOnNewDocument.*\n?/gi, '')
|
|
864
869
|
.replace(/.*chrome-devtools.*\n?/gi, '')
|
|
865
870
|
.replace(/.*webdriver.*\n?/gi, '')
|
|
866
871
|
.replace(/.*automation.*\n?/gi, '')
|
|
@@ -1023,8 +1028,9 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1023
1028
|
|
|
1024
1029
|
// Null context — return mock to prevent crashes
|
|
1025
1030
|
if (ctx === null) {
|
|
1026
|
-
|
|
1027
|
-
|
|
1031
|
+
const canvasEl = this; // capture the actual canvas element
|
|
1032
|
+
const mock = new Proxy({}, {
|
|
1033
|
+
get(target, prop) {
|
|
1028
1034
|
if (prop === 'getShaderPrecisionFormat') return () => ({ rangeMin: 127, rangeMax: 127, precision: 23 });
|
|
1029
1035
|
if (prop === 'getParameter') return (p) => webglSpoofParams[p] || 0;
|
|
1030
1036
|
if (prop === 'getSupportedExtensions') return () => [];
|
|
@@ -1032,12 +1038,28 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1032
1038
|
if (name === 'WEBGL_debug_renderer_info') return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
1033
1039
|
return null;
|
|
1034
1040
|
};
|
|
1035
|
-
if (prop === '
|
|
1036
|
-
|
|
1037
|
-
|
|
1041
|
+
if (prop === 'getContextAttributes') return () => ({
|
|
1042
|
+
alpha: true, antialias: true, depth: true, failIfMajorPerformanceCaveat: false,
|
|
1043
|
+
desynchronized: false, premultipliedAlpha: true, preserveDrawingBuffer: false,
|
|
1044
|
+
powerPreference: 'default', stencil: false, xrCompatible: false
|
|
1045
|
+
});
|
|
1046
|
+
if (prop === 'isContextLost') return () => false;
|
|
1047
|
+
if (prop === 'canvas') return canvasEl;
|
|
1048
|
+
if (prop === 'drawingBufferWidth') return canvasEl.width || 1920;
|
|
1049
|
+
if (prop === 'drawingBufferHeight') return canvasEl.height || 1080;
|
|
1050
|
+
if (prop === 'drawingBufferColorSpace') return 'srgb';
|
|
1051
|
+
// Identity — let prototype chain handle constructor/toString/Symbol.toStringTag
|
|
1052
|
+
if (prop === 'constructor') return WebGLRenderingContext;
|
|
1053
|
+
if (prop === Symbol.toStringTag) return 'WebGLRenderingContext';
|
|
1054
|
+
// Common draw/state methods — return noop to prevent crashes
|
|
1038
1055
|
return noop;
|
|
1039
1056
|
}
|
|
1040
1057
|
});
|
|
1058
|
+
// Make mock pass instanceof checks
|
|
1059
|
+
if (window.WebGLRenderingContext) {
|
|
1060
|
+
Object.setPrototypeOf(mock, WebGLRenderingContext.prototype);
|
|
1061
|
+
}
|
|
1062
|
+
return mock;
|
|
1041
1063
|
}
|
|
1042
1064
|
|
|
1043
1065
|
// Real context — wrap in Proxy to intercept getParameter/getExtension
|
|
@@ -1618,6 +1640,55 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
|
|
|
1618
1640
|
});
|
|
1619
1641
|
}, 'location URL masking');
|
|
1620
1642
|
|
|
1643
|
+
// Bulk-mask all spoofed prototype methods so toString() returns "[native code]"
|
|
1644
|
+
// Must run AFTER all overrides are applied
|
|
1645
|
+
safeExecute(() => {
|
|
1646
|
+
const protoMasks = [
|
|
1647
|
+
[WebGLRenderingContext.prototype, ['getParameter', 'getExtension', 'getSupportedExtensions']],
|
|
1648
|
+
[HTMLCanvasElement.prototype, ['getContext', 'toDataURL', 'toBlob']],
|
|
1649
|
+
[CanvasRenderingContext2D.prototype, ['getImageData', 'fillText', 'strokeText', 'measureText']],
|
|
1650
|
+
[EventTarget.prototype, ['addEventListener', 'removeEventListener']],
|
|
1651
|
+
[Date.prototype, ['getTimezoneOffset']],
|
|
1652
|
+
];
|
|
1653
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
1654
|
+
protoMasks.push([WebGL2RenderingContext.prototype, ['getParameter', 'getExtension']]);
|
|
1655
|
+
}
|
|
1656
|
+
protoMasks.forEach(([proto, methods]) => {
|
|
1657
|
+
methods.forEach(name => {
|
|
1658
|
+
if (typeof proto[name] === 'function') maskAsNative(proto[name], name);
|
|
1659
|
+
});
|
|
1660
|
+
});
|
|
1661
|
+
|
|
1662
|
+
// Mask navigator/window method overrides
|
|
1663
|
+
if (typeof navigator.permissions?.query === 'function') maskAsNative(navigator.permissions.query, 'query');
|
|
1664
|
+
if (typeof navigator.getBattery === 'function') maskAsNative(navigator.getBattery, 'getBattery');
|
|
1665
|
+
if (typeof speechSynthesis?.getVoices === 'function') maskAsNative(speechSynthesis.getVoices, 'getVoices');
|
|
1666
|
+
if (typeof performance.now === 'function') maskAsNative(performance.now, 'now');
|
|
1667
|
+
if (typeof Notification?.requestPermission === 'function') maskAsNative(Notification.requestPermission, 'requestPermission');
|
|
1668
|
+
if (typeof window.RTCPeerConnection === 'function') maskAsNative(window.RTCPeerConnection, 'RTCPeerConnection');
|
|
1669
|
+
if (typeof window.Image === 'function') maskAsNative(window.Image, 'Image');
|
|
1670
|
+
if (typeof window.fetch === 'function') maskAsNative(window.fetch, 'fetch');
|
|
1671
|
+
if (typeof window.PointerEvent === 'function') maskAsNative(window.PointerEvent, 'PointerEvent');
|
|
1672
|
+
|
|
1673
|
+
// Mask property getters on navigator
|
|
1674
|
+
const navProps = ['userAgentData', 'connection', 'pdfViewerEnabled', 'webdriver',
|
|
1675
|
+
'hardwareConcurrency', 'deviceMemory', 'platform', 'maxTouchPoints'];
|
|
1676
|
+
navProps.forEach(prop => {
|
|
1677
|
+
// Check both instance and prototype (webdriver lives on prototype)
|
|
1678
|
+
const desc = Object.getOwnPropertyDescriptor(navigator, prop)
|
|
1679
|
+
|| Object.getOwnPropertyDescriptor(Navigator.prototype, prop);
|
|
1680
|
+
if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
// Mask window property getters
|
|
1684
|
+
['screenX', 'screenY', 'outerWidth', 'outerHeight'].forEach(prop => {
|
|
1685
|
+
const desc = Object.getOwnPropertyDescriptor(window, prop);
|
|
1686
|
+
if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
|
|
1687
|
+
});
|
|
1688
|
+
|
|
1689
|
+
if (debugEnabled) console.log('[fingerprint] toString protection applied to all spoofed functions');
|
|
1690
|
+
}, 'Function.prototype.toString bulk masking');
|
|
1691
|
+
|
|
1621
1692
|
}, ua, forceDebug, selectedGpu);
|
|
1622
1693
|
} catch (stealthErr) {
|
|
1623
1694
|
if (stealthErr.message.includes('Session closed') ||
|
|
@@ -2013,9 +2084,6 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
|
|
|
2013
2084
|
}
|
|
2014
2085
|
|
|
2015
2086
|
// Legacy compatibility function - maintained for backwards compatibility
|
|
2016
|
-
function safeExecuteSpoofing(spoofFunction, description, forceDebug = false) {
|
|
2017
|
-
return safeSpoofingExecution(spoofFunction, description, { debug: forceDebug });
|
|
2018
|
-
}
|
|
2019
2087
|
|
|
2020
2088
|
|
|
2021
2089
|
module.exports = {
|
|
@@ -2027,7 +2095,6 @@ module.exports = {
|
|
|
2027
2095
|
applyAllFingerprintSpoofing,
|
|
2028
2096
|
simulateHumanBehavior,
|
|
2029
2097
|
safeDefineProperty,
|
|
2030
|
-
safeExecuteSpoofing, // Legacy compatibility
|
|
2031
2098
|
safeSpoofingExecution,
|
|
2032
2099
|
DEFAULT_PLATFORM,
|
|
2033
2100
|
DEFAULT_TIMEZONE
|
package/lib/interaction.js
CHANGED
|
@@ -123,6 +123,20 @@ const ELEMENT_INTERACTION = {
|
|
|
123
123
|
MISTAKE_RATE: 0.02 // Probability of typing mistakes (0.02 = 2% chance)
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
+
// === CONTENT CLICK CONSTANTS ===
|
|
127
|
+
// For triggering document-level onclick handlers (e.g., Monetag onclick_static)
|
|
128
|
+
// These clicks target the page content area, not specific UI elements
|
|
129
|
+
// NOTE: No preDelay needed — mouse movements + scrolling already provide ~1s
|
|
130
|
+
// of activity before clicks fire, which is enough for async ad script registration
|
|
131
|
+
const CONTENT_CLICK = {
|
|
132
|
+
CLICK_COUNT: 2, // Two attempts (primary + backup if first suppressed)
|
|
133
|
+
INTER_CLICK_MIN: 300, // Minimum ms between clicks (above Monetag 250ms cooldown)
|
|
134
|
+
INTER_CLICK_MAX: 500, // Maximum ms between clicks
|
|
135
|
+
PRE_CLICK_DELAY: 300, // Small buffer for late-loading async ad scripts
|
|
136
|
+
VIEWPORT_INSET: 0.2, // Avoid outer 20% of viewport (menus, overlays)
|
|
137
|
+
MOUSE_APPROACH_STEPS: 3 // Minimal steps — just enough for non-instant movement
|
|
138
|
+
};
|
|
139
|
+
|
|
126
140
|
// === INTENSITY SETTINGS ===
|
|
127
141
|
// Pre-configured intensity levels - modify these to change overall behavior
|
|
128
142
|
const INTENSITY_SETTINGS = {
|
|
@@ -606,6 +620,97 @@ async function interactWithElements(page, options = {}) {
|
|
|
606
620
|
}
|
|
607
621
|
}
|
|
608
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Clicks random spots in the page content area to trigger document-level
|
|
625
|
+
* onclick handlers (Monetag onclick_static, similar popunder SDKs).
|
|
626
|
+
*
|
|
627
|
+
* WHY THIS EXISTS:
|
|
628
|
+
* Ad onclick SDKs attach a single listener on `document` (capture phase)
|
|
629
|
+
* that fires on ANY click with `isTrusted: true`. They don't care which
|
|
630
|
+
* element was clicked — just that a real input event reached the document.
|
|
631
|
+
* `interactWithElements()` hunts for <button>/<a> which may not exist or
|
|
632
|
+
* may be excluded by the SDK's own filter. This function simply clicks
|
|
633
|
+
* the content area of the page where the SDK will always accept the event.
|
|
634
|
+
*
|
|
635
|
+
* TIMING:
|
|
636
|
+
* - 300ms preDelay: small buffer after mouse/scroll activity (~1.2s) for
|
|
637
|
+
* any late-loading async ad scripts to finish registering listeners.
|
|
638
|
+
* - Spaces clicks 300-500ms apart (above Monetag's 250ms cooldown).
|
|
639
|
+
* - Total time: ~1.1s for 2 clicks (preDelay + move + pause + click + gap).
|
|
640
|
+
*
|
|
641
|
+
* TARGETING:
|
|
642
|
+
* - Clicks within the inner 60% of the viewport to avoid sticky headers,
|
|
643
|
+
* footers, sidebars, cookie banners, and overlay close buttons.
|
|
644
|
+
* - Each click gets a fresh random position with natural mouse approach.
|
|
645
|
+
*
|
|
646
|
+
* @param {import('puppeteer').Page} page
|
|
647
|
+
* @param {object} [options]
|
|
648
|
+
* @param {number} [options.clicks] Number of click attempts
|
|
649
|
+
* @param {number} [options.preDelay] Ms to wait before first click
|
|
650
|
+
* @param {number} [options.interClickMin] Min ms between clicks
|
|
651
|
+
* @param {number} [options.interClickMax] Max ms between clicks
|
|
652
|
+
* @param {boolean} [options.forceDebug] Log click coordinates
|
|
653
|
+
*/
|
|
654
|
+
async function performContentClicks(page, options = {}) {
|
|
655
|
+
const {
|
|
656
|
+
clicks = CONTENT_CLICK.CLICK_COUNT,
|
|
657
|
+
preDelay = CONTENT_CLICK.PRE_CLICK_DELAY,
|
|
658
|
+
interClickMin = CONTENT_CLICK.INTER_CLICK_MIN,
|
|
659
|
+
interClickMax = CONTENT_CLICK.INTER_CLICK_MAX,
|
|
660
|
+
forceDebug = false
|
|
661
|
+
} = options;
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
if (page.isClosed()) return;
|
|
665
|
+
|
|
666
|
+
const viewport = await getCachedViewport(page);
|
|
667
|
+
const inset = CONTENT_CLICK.VIEWPORT_INSET;
|
|
668
|
+
const minX = Math.floor(viewport.width * inset);
|
|
669
|
+
const maxX = Math.floor(viewport.width * (1 - inset));
|
|
670
|
+
const minY = Math.floor(viewport.height * inset);
|
|
671
|
+
const maxY = Math.floor(viewport.height * (1 - inset));
|
|
672
|
+
|
|
673
|
+
// Wait for ad scripts to register their listeners
|
|
674
|
+
await fastTimeout(preDelay);
|
|
675
|
+
|
|
676
|
+
let lastX = minX + Math.floor(Math.random() * (maxX - minX));
|
|
677
|
+
let lastY = minY + Math.floor(Math.random() * (maxY - minY));
|
|
678
|
+
|
|
679
|
+
for (let i = 0; i < clicks; i++) {
|
|
680
|
+
if (page.isClosed()) break;
|
|
681
|
+
|
|
682
|
+
// Random position in content zone
|
|
683
|
+
const targetX = minX + Math.floor(Math.random() * (maxX - minX));
|
|
684
|
+
const targetY = minY + Math.floor(Math.random() * (maxY - minY));
|
|
685
|
+
|
|
686
|
+
// Natural mouse approach (few steps, no need for elaborate curves)
|
|
687
|
+
await humanLikeMouseMove(page, lastX, lastY, targetX, targetY, {
|
|
688
|
+
steps: CONTENT_CLICK.MOUSE_APPROACH_STEPS,
|
|
689
|
+
curve: 0.03 + Math.random() * 0.04,
|
|
690
|
+
jitter: 1
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Brief human-like pause, then click
|
|
694
|
+
await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN));
|
|
695
|
+
await page.mouse.click(targetX, targetY);
|
|
696
|
+
|
|
697
|
+
if (forceDebug) {
|
|
698
|
+
console.log(`[interaction] Content click ${i + 1}/${clicks} at (${targetX}, ${targetY})`);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
lastX = targetX;
|
|
702
|
+
lastY = targetY;
|
|
703
|
+
|
|
704
|
+
// Inter-click gap (skip after last click)
|
|
705
|
+
if (i < clicks - 1) {
|
|
706
|
+
await fastTimeout(interClickMin + Math.random() * (interClickMax - interClickMin));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch (err) {
|
|
710
|
+
// Content clicks are supplementary — never break the scan
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
609
714
|
/**
|
|
610
715
|
* Simulates realistic typing behavior with human characteristics
|
|
611
716
|
*
|
|
@@ -876,12 +981,22 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
|
|
|
876
981
|
}
|
|
877
982
|
}
|
|
878
983
|
|
|
879
|
-
//
|
|
984
|
+
// Click interaction — two strategies for maximum ad script coverage
|
|
985
|
+
// 1. Content area clicks: triggers document-level onclick handlers
|
|
986
|
+
// (Monetag, similar popunder SDKs that listen on document)
|
|
987
|
+
// 2. Element clicks: interacts with specific UI elements
|
|
988
|
+
// (ad scripts that attach to specific clickable elements)
|
|
880
989
|
if (includeElementClicks) {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
990
|
+
if (checkTimeout()) return; // Emergency timeout check
|
|
991
|
+
// Primary: content area clicks for document-level onclick handlers
|
|
992
|
+
await performContentClicks(page, { forceDebug });
|
|
993
|
+
// Secondary: targeted element clicks (fast, 1 attempt only)
|
|
994
|
+
if (!checkTimeout()) {
|
|
995
|
+
await interactWithElements(page, {
|
|
996
|
+
maxAttempts: 1,
|
|
997
|
+
avoidDestructive: true
|
|
998
|
+
});
|
|
999
|
+
}
|
|
885
1000
|
}
|
|
886
1001
|
|
|
887
1002
|
// Periodic memory cleanup during interaction
|
|
@@ -1105,6 +1220,7 @@ module.exports = {
|
|
|
1105
1220
|
humanLikeMouseMove,
|
|
1106
1221
|
simulateScrolling,
|
|
1107
1222
|
interactWithElements,
|
|
1223
|
+
performContentClicks,
|
|
1108
1224
|
simulateTyping,
|
|
1109
1225
|
generateRandomCoordinates
|
|
1110
1226
|
};
|
package/lib/nettools.js
CHANGED
|
@@ -11,6 +11,20 @@ const execPromise = util.promisify(exec);
|
|
|
11
11
|
// Cycling index for whois server rotation
|
|
12
12
|
let whoisServerCycleIndex = 0;
|
|
13
13
|
|
|
14
|
+
// Global dig result cache — shared across ALL handler instances and processUrl calls
|
|
15
|
+
// Key: `${domain}-${recordType}`, Value: { result, timestamp }
|
|
16
|
+
// DNS records don't change based on what terms you're searching for,
|
|
17
|
+
// so we cache the raw dig output and let each handler check its own terms against it
|
|
18
|
+
const globalDigResultCache = new Map();
|
|
19
|
+
const GLOBAL_DIG_CACHE_TTL = 300000; // 5 minutes
|
|
20
|
+
const GLOBAL_DIG_CACHE_MAX = 500;
|
|
21
|
+
|
|
22
|
+
// Global whois result cache — shared across ALL handler instances and processUrl calls
|
|
23
|
+
// Whois data is per root domain and doesn't change based on search terms
|
|
24
|
+
const globalWhoisResultCache = new Map();
|
|
25
|
+
const GLOBAL_WHOIS_CACHE_TTL = 900000; // 15 minutes (whois data changes less frequently)
|
|
26
|
+
const GLOBAL_WHOIS_CACHE_MAX = 500;
|
|
27
|
+
|
|
14
28
|
/**
|
|
15
29
|
* Strips ANSI color codes from a string for clean file logging
|
|
16
30
|
* @param {string} text - Text that may contain ANSI codes
|
|
@@ -26,27 +40,28 @@ function stripAnsiColors(text) {
|
|
|
26
40
|
* @returns {Object} Object with isAvailable boolean and version/error info
|
|
27
41
|
*/
|
|
28
42
|
function validateWhoisAvailability() {
|
|
43
|
+
if (validateWhoisAvailability._cached) return validateWhoisAvailability._cached;
|
|
29
44
|
try {
|
|
30
45
|
const result = execSync('whois --version 2>&1', { encoding: 'utf8' });
|
|
31
|
-
|
|
46
|
+
validateWhoisAvailability._cached = {
|
|
32
47
|
isAvailable: true,
|
|
33
48
|
version: result.trim()
|
|
34
49
|
};
|
|
35
50
|
} catch (error) {
|
|
36
|
-
// Some systems don't have --version, try just whois
|
|
37
51
|
try {
|
|
38
52
|
execSync('which whois', { encoding: 'utf8' });
|
|
39
|
-
|
|
53
|
+
validateWhoisAvailability._cached = {
|
|
40
54
|
isAvailable: true,
|
|
41
55
|
version: 'whois (version unknown)'
|
|
42
56
|
};
|
|
43
57
|
} catch (e) {
|
|
44
|
-
|
|
58
|
+
validateWhoisAvailability._cached = {
|
|
45
59
|
isAvailable: false,
|
|
46
60
|
error: 'whois command not found'
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
}
|
|
64
|
+
return validateWhoisAvailability._cached;
|
|
50
65
|
}
|
|
51
66
|
|
|
52
67
|
/**
|
|
@@ -54,18 +69,20 @@ function validateWhoisAvailability() {
|
|
|
54
69
|
* @returns {Object} Object with isAvailable boolean and version/error info
|
|
55
70
|
*/
|
|
56
71
|
function validateDigAvailability() {
|
|
72
|
+
if (validateDigAvailability._cached) return validateDigAvailability._cached;
|
|
57
73
|
try {
|
|
58
74
|
const result = execSync('dig -v 2>&1', { encoding: 'utf8' });
|
|
59
|
-
|
|
75
|
+
validateDigAvailability._cached = {
|
|
60
76
|
isAvailable: true,
|
|
61
77
|
version: result.split('\n')[0].trim()
|
|
62
78
|
};
|
|
63
79
|
} catch (error) {
|
|
64
|
-
|
|
80
|
+
validateDigAvailability._cached = {
|
|
65
81
|
isAvailable: false,
|
|
66
82
|
error: 'dig command not found'
|
|
67
83
|
};
|
|
68
84
|
}
|
|
85
|
+
return validateDigAvailability._cached;
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
/**
|
|
@@ -635,8 +652,8 @@ async function digLookup(domain = '', recordType = 'A', timeout = 5000) {
|
|
|
635
652
|
// Clean domain
|
|
636
653
|
const cleanDomain = domain.replace(/^https?:\/\//, '').replace(/\/.*$/, '').replace(/:\d+$/, '');
|
|
637
654
|
|
|
638
|
-
//
|
|
639
|
-
const { stdout, stderr } = await execWithTimeout(`dig
|
|
655
|
+
// Single dig command — full output contains everything including the short answers
|
|
656
|
+
const { stdout: fullOutput, stderr } = await execWithTimeout(`dig "${cleanDomain}" ${recordType}`, timeout);
|
|
640
657
|
|
|
641
658
|
if (stderr && stderr.trim()) {
|
|
642
659
|
return {
|
|
@@ -647,13 +664,21 @@ async function digLookup(domain = '', recordType = 'A', timeout = 5000) {
|
|
|
647
664
|
};
|
|
648
665
|
}
|
|
649
666
|
|
|
650
|
-
//
|
|
651
|
-
const
|
|
667
|
+
// Extract short output from ANSWER SECTION of full dig output
|
|
668
|
+
const answerMatch = fullOutput.match(/;; ANSWER SECTION:\n([\s\S]*?)(?:\n;;|\n*$)/);
|
|
669
|
+
let shortOutput = '';
|
|
670
|
+
if (answerMatch) {
|
|
671
|
+
shortOutput = answerMatch[1]
|
|
672
|
+
.split('\n')
|
|
673
|
+
.map(line => line.split(/\s+/).pop())
|
|
674
|
+
.filter(Boolean)
|
|
675
|
+
.join('\n');
|
|
676
|
+
}
|
|
652
677
|
|
|
653
678
|
return {
|
|
654
679
|
success: true,
|
|
655
680
|
output: fullOutput,
|
|
656
|
-
shortOutput
|
|
681
|
+
shortOutput,
|
|
657
682
|
domain: cleanDomain,
|
|
658
683
|
recordType
|
|
659
684
|
};
|
|
@@ -766,7 +791,6 @@ function createNetToolsHandler(config) {
|
|
|
766
791
|
whoisDelay = 4000,
|
|
767
792
|
whoisServer,
|
|
768
793
|
whoisServerMode = 'random',
|
|
769
|
-
bufferedLogWrite = null,
|
|
770
794
|
debugLogFile = null,
|
|
771
795
|
digTerms,
|
|
772
796
|
digOrTerms,
|
|
@@ -807,20 +831,10 @@ function createNetToolsHandler(config) {
|
|
|
807
831
|
subdomain: digSubdomain
|
|
808
832
|
});
|
|
809
833
|
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
// Size Memory
|
|
815
|
-
// 100 ~900KB
|
|
816
|
-
// 200 1.8MB
|
|
817
|
-
// 300 2.6MB
|
|
818
|
-
// 400 3.4MB
|
|
819
|
-
// 500 4.2MB
|
|
820
|
-
// Add DNS resolution caching to avoid redundant dig lookups
|
|
821
|
-
const digResultCache = new Map();
|
|
822
|
-
const DIG_CACHE_TTL = 300000; // 5 minutes cache TTL
|
|
823
|
-
const DIG_MAX_CACHE_SIZE = 400; // Smaller cache for dig due to shorter TTL
|
|
834
|
+
// Whois cache is global (globalWhoisResultCache) — shared across all handler instances
|
|
835
|
+
// Whois data is per root domain and doesn't change based on search terms
|
|
836
|
+
// Dig cache is global (globalDigResultCache) — shared across all handler instances
|
|
837
|
+
// DNS results are the same regardless of search terms
|
|
824
838
|
|
|
825
839
|
return async function handleNetToolsCheck(domain, fullSubdomain) {
|
|
826
840
|
// Use fullSubdomain parameter instead of originalDomain to maintain consistency
|
|
@@ -844,19 +858,17 @@ function createNetToolsHandler(config) {
|
|
|
844
858
|
|
|
845
859
|
// Move the logToConsoleAndFile function declaration from later in the file to here:
|
|
846
860
|
function logToConsoleAndFile(message) {
|
|
861
|
+
// Note: This function needs access to forceDebug, debugLogFile, and fs from the parent scope
|
|
862
|
+
// These are passed in via the config object to createNetToolsHandler
|
|
863
|
+
// forceDebug, debugLogFile, and fs are available in this closure
|
|
864
|
+
|
|
865
|
+
// Always log to console when in debug mode
|
|
847
866
|
if (forceDebug) {
|
|
848
867
|
console.log(formatLogMessage('debug', message));
|
|
849
868
|
}
|
|
850
869
|
|
|
851
|
-
if
|
|
852
|
-
|
|
853
|
-
const timestamp = new Date().toISOString();
|
|
854
|
-
const cleanMessage = stripAnsiColors(message);
|
|
855
|
-
bufferedLogWrite(debugLogFile, `${timestamp} [debug nettools] ${cleanMessage}\n`);
|
|
856
|
-
} catch (logErr) {
|
|
857
|
-
// Silently fail file logging to avoid disrupting whois operations
|
|
858
|
-
}
|
|
859
|
-
} else if (debugLogFile && fs) {
|
|
870
|
+
// Also log to file if debug file logging is enabled
|
|
871
|
+
if (debugLogFile && fs) {
|
|
860
872
|
try {
|
|
861
873
|
const timestamp = new Date().toISOString();
|
|
862
874
|
const cleanMessage = stripAnsiColors(message);
|
|
@@ -982,9 +994,9 @@ function createNetToolsHandler(config) {
|
|
|
982
994
|
const now = Date.now();
|
|
983
995
|
let whoisResult = null;
|
|
984
996
|
|
|
985
|
-
if (
|
|
986
|
-
const cachedEntry =
|
|
987
|
-
if (now - cachedEntry.timestamp <
|
|
997
|
+
if (globalWhoisResultCache.has(whoisCacheKey)) {
|
|
998
|
+
const cachedEntry = globalWhoisResultCache.get(whoisCacheKey);
|
|
999
|
+
if (now - cachedEntry.timestamp < GLOBAL_WHOIS_CACHE_TTL) {
|
|
988
1000
|
if (forceDebug) {
|
|
989
1001
|
const age = Math.round((now - cachedEntry.timestamp) / 1000);
|
|
990
1002
|
const serverInfo = (selectedServer && selectedServer !== '') ? ` (server: ${selectedServer})` : ' (default server)';
|
|
@@ -998,7 +1010,7 @@ function createNetToolsHandler(config) {
|
|
|
998
1010
|
});
|
|
999
1011
|
} else {
|
|
1000
1012
|
// Cache expired, remove it
|
|
1001
|
-
|
|
1013
|
+
globalWhoisResultCache.delete(whoisCacheKey);
|
|
1002
1014
|
if (forceDebug) {
|
|
1003
1015
|
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cache expired for ${whoisRootDomain}, performing fresh lookup`);
|
|
1004
1016
|
}
|
|
@@ -1030,7 +1042,7 @@ function createNetToolsHandler(config) {
|
|
|
1030
1042
|
!whoisResult.error.toLowerCase().includes('connection') &&
|
|
1031
1043
|
!whoisResult.error.toLowerCase().includes('network'))) {
|
|
1032
1044
|
|
|
1033
|
-
|
|
1045
|
+
globalWhoisResultCache.set(whoisCacheKey, {
|
|
1034
1046
|
result: whoisResult,
|
|
1035
1047
|
timestamp: now
|
|
1036
1048
|
});
|
|
@@ -1183,17 +1195,17 @@ function createNetToolsHandler(config) {
|
|
|
1183
1195
|
}
|
|
1184
1196
|
|
|
1185
1197
|
// Periodic whois cache cleanup to prevent memory leaks
|
|
1186
|
-
if (
|
|
1198
|
+
if (globalWhoisResultCache.size > GLOBAL_WHOIS_CACHE_MAX) {
|
|
1187
1199
|
const now = Date.now();
|
|
1188
1200
|
let cleanedCount = 0;
|
|
1189
|
-
for (const [key, entry] of
|
|
1190
|
-
if (now - entry.timestamp >
|
|
1191
|
-
|
|
1201
|
+
for (const [key, entry] of globalWhoisResultCache.entries()) {
|
|
1202
|
+
if (now - entry.timestamp > GLOBAL_WHOIS_CACHE_TTL) {
|
|
1203
|
+
globalWhoisResultCache.delete(key);
|
|
1192
1204
|
cleanedCount++;
|
|
1193
1205
|
}
|
|
1194
1206
|
}
|
|
1195
1207
|
if (forceDebug && cleanedCount > 0) {
|
|
1196
|
-
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${
|
|
1208
|
+
logToConsoleAndFile(`${messageColors.highlight('[whois-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${globalWhoisResultCache.size}`);
|
|
1197
1209
|
}
|
|
1198
1210
|
}
|
|
1199
1211
|
}
|
|
@@ -1216,16 +1228,16 @@ function createNetToolsHandler(config) {
|
|
|
1216
1228
|
const now = Date.now();
|
|
1217
1229
|
let digResult = null;
|
|
1218
1230
|
|
|
1219
|
-
if (
|
|
1220
|
-
const cachedEntry =
|
|
1221
|
-
if (now - cachedEntry.timestamp <
|
|
1231
|
+
if (globalDigResultCache.has(digCacheKey)) {
|
|
1232
|
+
const cachedEntry = globalDigResultCache.get(digCacheKey);
|
|
1233
|
+
if (now - cachedEntry.timestamp < GLOBAL_DIG_CACHE_TTL) {
|
|
1222
1234
|
if (forceDebug) {
|
|
1223
1235
|
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Using cached result for ${digDomain} (${digRecordType}) [age: ${Math.round((now - cachedEntry.timestamp) / 1000)}s]`);
|
|
1224
1236
|
}
|
|
1225
1237
|
digResult = cachedEntry.result;
|
|
1226
1238
|
} else {
|
|
1227
1239
|
// Cache expired, remove it
|
|
1228
|
-
|
|
1240
|
+
globalDigResultCache.delete(digCacheKey);
|
|
1229
1241
|
}
|
|
1230
1242
|
}
|
|
1231
1243
|
|
|
@@ -1233,7 +1245,7 @@ function createNetToolsHandler(config) {
|
|
|
1233
1245
|
digResult = await digLookup(digDomain, digRecordType, 5000); // 5 second timeout for dig
|
|
1234
1246
|
|
|
1235
1247
|
// Cache the result for future use
|
|
1236
|
-
|
|
1248
|
+
globalDigResultCache.set(digCacheKey, {
|
|
1237
1249
|
result: digResult,
|
|
1238
1250
|
timestamp: now
|
|
1239
1251
|
});
|
|
@@ -1303,17 +1315,17 @@ function createNetToolsHandler(config) {
|
|
|
1303
1315
|
}
|
|
1304
1316
|
|
|
1305
1317
|
// Periodic dig cache cleanup to prevent memory leaks
|
|
1306
|
-
if (
|
|
1318
|
+
if (globalDigResultCache.size > GLOBAL_DIG_CACHE_MAX) {
|
|
1307
1319
|
const now = Date.now();
|
|
1308
1320
|
let cleanedCount = 0;
|
|
1309
|
-
for (const [key, entry] of
|
|
1310
|
-
if (now - entry.timestamp >
|
|
1311
|
-
|
|
1321
|
+
for (const [key, entry] of globalDigResultCache.entries()) {
|
|
1322
|
+
if (now - entry.timestamp > GLOBAL_DIG_CACHE_TTL) {
|
|
1323
|
+
globalDigResultCache.delete(key);
|
|
1312
1324
|
cleanedCount++;
|
|
1313
1325
|
}
|
|
1314
1326
|
}
|
|
1315
1327
|
if (forceDebug && cleanedCount > 0) {
|
|
1316
|
-
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${
|
|
1328
|
+
logToConsoleAndFile(`${messageColors.highlight('[dig-cache]')} Cleaned ${cleanedCount} expired entries, cache size: ${globalDigResultCache.size}`);
|
|
1317
1329
|
}
|
|
1318
1330
|
}
|
|
1319
1331
|
}
|
|
@@ -1387,12 +1399,7 @@ function createNetToolsHandler(config) {
|
|
|
1387
1399
|
|
|
1388
1400
|
// Add whois server info to log if custom server was used
|
|
1389
1401
|
const serverInfo = whoisServer ? ` (whois-server: ${selectWhoisServer(whoisServer)})` : '';
|
|
1390
|
-
|
|
1391
|
-
if (bufferedLogWrite) {
|
|
1392
|
-
bufferedLogWrite(matchedUrlsLogFile, logLine);
|
|
1393
|
-
} else {
|
|
1394
|
-
fs.appendFileSync(matchedUrlsLogFile, logLine);
|
|
1395
|
-
}
|
|
1402
|
+
fs.appendFileSync(matchedUrlsLogFile, `${timestamp} [match][${simplifiedUrl}] ${domain} (${matchType.join(' + ')})${serverInfo}\n`);
|
|
1396
1403
|
}
|
|
1397
1404
|
}
|
|
1398
1405
|
|
package/nwss.js
CHANGED
|
@@ -39,7 +39,7 @@ const { processResults } = require('./lib/post-processing');
|
|
|
39
39
|
// Colorize various text when used
|
|
40
40
|
const { colorize, colors, messageColors, tags, formatLogMessage } = require('./lib/colorize');
|
|
41
41
|
// Enhanced mouse interaction and page simulation
|
|
42
|
-
const { performPageInteraction, createInteractionConfig } = require('./lib/interaction');
|
|
42
|
+
const { performPageInteraction, createInteractionConfig, performContentClicks, humanLikeMouseMove } = require('./lib/interaction');
|
|
43
43
|
// Domain detection cache for performance optimization
|
|
44
44
|
const { createGlobalHelpers, getTotalDomainsSkipped, getDetectedDomainsCount } = require('./lib/domain-cache');
|
|
45
45
|
const { createSmartCache } = require('./lib/smart-cache'); // Smart cache system
|
|
@@ -1447,7 +1447,6 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
1447
1447
|
'--disable-features=SafeBrowsing',
|
|
1448
1448
|
'--disable-dev-shm-usage',
|
|
1449
1449
|
'--disable-sync',
|
|
1450
|
-
'--use-gl=swiftshader', // Software WebGL — prevents ad script crashes in headless
|
|
1451
1450
|
'--mute-audio',
|
|
1452
1451
|
'--disable-translate',
|
|
1453
1452
|
'--window-size=1920,1080',
|
|
@@ -2730,9 +2729,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2730
2729
|
whoisDelay: siteConfig.whois_delay !== undefined ? siteConfig.whois_delay : whois_delay,
|
|
2731
2730
|
whoisServer,
|
|
2732
2731
|
whoisServerMode: siteConfig.whois_server_mode || whois_server_mode,
|
|
2733
|
-
bufferedLogWrite,
|
|
2734
2732
|
debugLogFile,
|
|
2735
|
-
fs,
|
|
2736
2733
|
digTerms,
|
|
2737
2734
|
digOrTerms,
|
|
2738
2735
|
digRecordType,
|
|
@@ -2838,9 +2835,7 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
2838
2835
|
whoisDelay: siteConfig.whois_delay !== undefined ? siteConfig.whois_delay : whois_delay, // Site-specific or global fallback
|
|
2839
2836
|
whoisServer, // Pass whois server configuration
|
|
2840
2837
|
whoisServerMode: siteConfig.whois_server_mode || whois_server_mode,
|
|
2841
|
-
|
|
2842
|
-
debugLogFile, // Pass debug log file for whois error logging
|
|
2843
|
-
fs, // Pass fs module for file operations
|
|
2838
|
+
debugLogFile,
|
|
2844
2839
|
digTerms,
|
|
2845
2840
|
digOrTerms,
|
|
2846
2841
|
digRecordType,
|
|
@@ -3664,6 +3659,32 @@ function setupFrameHandling(page, forceDebug) {
|
|
|
3664
3659
|
}
|
|
3665
3660
|
}
|
|
3666
3661
|
|
|
3662
|
+
// Post-reload interaction: trigger onclick ad scripts (Monetag etc.)
|
|
3663
|
+
// Each reload gives a fresh session with a new random ad domain —
|
|
3664
|
+
// without clicks the SDK never fires and we miss those domains.
|
|
3665
|
+
if (interactEnabled && !page.isClosed()) {
|
|
3666
|
+
try {
|
|
3667
|
+
// Brief wait for ad scripts to re-register after reload
|
|
3668
|
+
await fastTimeout(800);
|
|
3669
|
+
// Quick mouse moves to build movement score (Monetag tracks this)
|
|
3670
|
+
const vp = page.viewport() || { width: 1920, height: 1080 };
|
|
3671
|
+
const startX = 200 + Math.floor(Math.random() * (vp.width - 400));
|
|
3672
|
+
const startY = 200 + Math.floor(Math.random() * (vp.height - 400));
|
|
3673
|
+
await page.mouse.move(startX, startY);
|
|
3674
|
+
for (let m = 0; m < 2; m++) {
|
|
3675
|
+
const endX = 200 + Math.floor(Math.random() * (vp.width - 400));
|
|
3676
|
+
const endY = 200 + Math.floor(Math.random() * (vp.height - 400));
|
|
3677
|
+
await humanLikeMouseMove(page, startX, startY, endX, endY, { steps: 3, curve: 0.04, jitter: 1 });
|
|
3678
|
+
}
|
|
3679
|
+
// Content clicks to trigger document-level onclick handlers
|
|
3680
|
+
await performContentClicks(page, { clicks: 2, preDelay: 200, forceDebug });
|
|
3681
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction completed for reload #${i}`));
|
|
3682
|
+
} catch (postReloadInteractErr) {
|
|
3683
|
+
// Non-critical — continue with remaining reloads
|
|
3684
|
+
if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction failed: ${postReloadInteractErr.message}`));
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3667
3688
|
// Only add delay if we're continuing with more reloads
|
|
3668
3689
|
if (i < totalReloads) {
|
|
3669
3690
|
// Reduce delay for problematic sites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanboynz/network-scanner",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.55",
|
|
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": {
|