@js-recon/js-recon 2.0.1-alpha.2 → 2.0.1-alpha.3
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/AGENTS.md +694 -0
- package/CHANGELOG.md +17 -0
- package/build/analyze/helpers/initRules.js +19 -17
- package/build/analyze/helpers/initRules.js.map +1 -1
- package/build/globalConfig.js +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/lazyLoad/downloadFilesUtil.js +7 -1
- package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
- package/build/lazyLoad/downloadQueue.js +7 -1
- package/build/lazyLoad/downloadQueue.js.map +1 -1
- package/build/lazyLoad/downloadResponse.js +9 -1
- package/build/lazyLoad/downloadResponse.js.map +1 -1
- package/build/lazyLoad/index.js +78 -6
- package/build/lazyLoad/index.js.map +1 -1
- package/build/lazyLoad/outputPath.js +9 -1
- package/build/lazyLoad/outputPath.js.map +1 -1
- package/build/lazyLoad/react/react_followImports.js +20 -3
- package/build/lazyLoad/react/react_followImports.js.map +1 -1
- package/build/lazyLoad/shared/webpackChunkParsers.js +115 -0
- package/build/lazyLoad/shared/webpackChunkParsers.js.map +1 -1
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js +59 -5
- package/build/lazyLoad/svelte/svelte_getFromPageSource.js.map +1 -1
- package/build/lazyLoad/techDetect/checkAngularDevServer.js +51 -0
- package/build/lazyLoad/techDetect/checkAngularDevServer.js.map +1 -0
- package/build/lazyLoad/techDetect/checkNextDevServer.js +25 -0
- package/build/lazyLoad/techDetect/checkNextDevServer.js.map +1 -0
- package/build/lazyLoad/techDetect/checkReactDevServer.js +93 -0
- package/build/lazyLoad/techDetect/checkReactDevServer.js.map +1 -0
- package/build/lazyLoad/techDetect/checkSvelteDevServer.js +32 -0
- package/build/lazyLoad/techDetect/checkSvelteDevServer.js.map +1 -0
- package/build/lazyLoad/techDetect/checkVueDevServer.js +51 -0
- package/build/lazyLoad/techDetect/checkVueDevServer.js.map +1 -0
- package/build/lazyLoad/techDetect/index.js +44 -18
- package/build/lazyLoad/techDetect/index.js.map +1 -1
- package/build/lazyLoad/vue/vue_jsImports.js +23 -9
- package/build/lazyLoad/vue/vue_jsImports.js.map +1 -1
- package/build/run/index.js +52 -11
- package/build/run/index.js.map +1 -1
- package/build/utility/fatalHandlers.js +29 -0
- package/build/utility/fatalHandlers.js.map +1 -0
- package/package.json +1 -2
- package/scripts/dev-server-benchmark-utils.mjs +24 -0
- package/scripts/dev-server-benchmark.mjs +225 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL-normalization helpers for dev-server-benchmark.mjs, split into their own
|
|
3
|
+
* side-effect-free module so they can be unit tested without triggering the parent
|
|
4
|
+
* script's CLI arg parsing / katana-on-PATH checks (which call process.exit on import).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function normalize(rawUrl) {
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(rawUrl.trim());
|
|
10
|
+
const collapsedPath = u.pathname.replace(/\/{2,}/g, "/");
|
|
11
|
+
return `${u.origin}${collapsedPath}`;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeSet(urls) {
|
|
18
|
+
const out = new Set();
|
|
19
|
+
for (const raw of urls) {
|
|
20
|
+
const n = normalize(raw);
|
|
21
|
+
if (n) out.add(n);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Benchmarks js-recon's dev-server lazyload coverage (e.g. `vue-dev`, `next-dev`) against an
|
|
4
|
+
* independent ground truth crawl from katana, for one or more live dev-server targets.
|
|
5
|
+
*
|
|
6
|
+
* Generalizes the one-off shell-script pipeline used for the #183 Svelte benchmark
|
|
7
|
+
* (js-recon-tests/run_svelte_katana_benchmark_driver.sh + set-diff-normalize.mjs) into
|
|
8
|
+
* reusable tooling shipped with js-recon itself, so dev-server discovery coverage can be
|
|
9
|
+
* re-checked as a regression test whenever the crawler changes (js-recon-internal-docs#190).
|
|
10
|
+
*
|
|
11
|
+
* Requires the `katana` binary on PATH (https://github.com/projectdiscovery/katana).
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* node scripts/dev-server-benchmark.mjs <targets-file> [options]
|
|
15
|
+
* node scripts/dev-server-benchmark.mjs --url <single-target> [options]
|
|
16
|
+
*
|
|
17
|
+
* Options:
|
|
18
|
+
* --url <url> Benchmark a single target instead of reading a targets file.
|
|
19
|
+
* --threads <n> Threads passed to js-recon's lazyload (default: 2 — a higher thread
|
|
20
|
+
* count was observed to trigger a response-cache race against Vite dev
|
|
21
|
+
* servers under concurrent load; see the vue-dev-server-support.md doc).
|
|
22
|
+
* --output <file> Write the full JSON report to this file (default: stdout summary only).
|
|
23
|
+
* --insecure Pass -k (ignore TLS errors) to both katana and js-recon.
|
|
24
|
+
* --keep-output-dirs Don't delete the per-target js-recon output directories afterward.
|
|
25
|
+
*
|
|
26
|
+
* Targets file format: one dev-server URL per line, blank lines and #-comments ignored.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { spawnSync } from "child_process";
|
|
30
|
+
import { readFileSync, writeFileSync, mkdtempSync, rmSync, existsSync } from "fs";
|
|
31
|
+
import { tmpdir } from "os";
|
|
32
|
+
import { join, dirname } from "path";
|
|
33
|
+
import { fileURLToPath } from "url";
|
|
34
|
+
import { normalize, normalizeSet } from "./dev-server-benchmark-utils.mjs";
|
|
35
|
+
|
|
36
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const jsReconEntry = join(__dirname, "..", "build", "index.js");
|
|
38
|
+
|
|
39
|
+
const JS_FILE_RE = /\.(m?js|jsx|tsx?|vue)(\?|$)/i;
|
|
40
|
+
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
const opts = { threads: "2", output: null, insecure: false, keepOutputDirs: false, url: null, targetsFile: null };
|
|
43
|
+
for (let i = 0; i < args.length; i++) {
|
|
44
|
+
const a = args[i];
|
|
45
|
+
if (a === "--url") opts.url = args[++i];
|
|
46
|
+
else if (a === "--threads") opts.threads = args[++i];
|
|
47
|
+
else if (a === "--output") opts.output = args[++i];
|
|
48
|
+
else if (a === "--insecure") opts.insecure = true;
|
|
49
|
+
else if (a === "--keep-output-dirs") opts.keepOutputDirs = true;
|
|
50
|
+
else if (!a.startsWith("--")) opts.targetsFile = a;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!opts.url && !opts.targetsFile) {
|
|
54
|
+
console.error("Usage: node scripts/dev-server-benchmark.mjs <targets-file> [options]");
|
|
55
|
+
console.error(" node scripts/dev-server-benchmark.mjs --url <single-target> [options]");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!existsSync(jsReconEntry)) {
|
|
60
|
+
console.error(`[!] ${jsReconEntry} not found. Run \`npm run cleanup\` (or \`tsc\`) first.`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const katanaCheck = spawnSync("katana", ["-version"], { stdio: "ignore" });
|
|
65
|
+
if (katanaCheck.error) {
|
|
66
|
+
console.error("[!] katana binary not found on PATH. Install it: https://github.com/projectdiscovery/katana");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readTargets() {
|
|
71
|
+
if (opts.url) return [opts.url];
|
|
72
|
+
return readFileSync(opts.targetsFile, "utf8")
|
|
73
|
+
.split("\n")
|
|
74
|
+
.map((l) => l.trim())
|
|
75
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runKatana(target) {
|
|
79
|
+
const outFile = join(tmpdir(), `jsr-katana-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
80
|
+
try {
|
|
81
|
+
const katanaArgs = [
|
|
82
|
+
"-u",
|
|
83
|
+
target,
|
|
84
|
+
"-hl",
|
|
85
|
+
"-jsl",
|
|
86
|
+
"-jc",
|
|
87
|
+
"-fs",
|
|
88
|
+
"fqdn",
|
|
89
|
+
"-d",
|
|
90
|
+
"3",
|
|
91
|
+
"-mdp",
|
|
92
|
+
"300",
|
|
93
|
+
"-ct",
|
|
94
|
+
"5m",
|
|
95
|
+
"-silent",
|
|
96
|
+
"-o",
|
|
97
|
+
outFile,
|
|
98
|
+
];
|
|
99
|
+
if (opts.insecure) katanaArgs.push("-tlsi");
|
|
100
|
+
const result = spawnSync("katana", katanaArgs, { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
101
|
+
if (result.error) throw new Error(`katana failed to run: ${result.error.message}`);
|
|
102
|
+
if (result.status !== 0) {
|
|
103
|
+
throw new Error(`katana exited with status ${result.status}: ${(result.stderr || "").trim()}`);
|
|
104
|
+
}
|
|
105
|
+
if (!existsSync(outFile)) return [];
|
|
106
|
+
const lines = readFileSync(outFile, "utf8")
|
|
107
|
+
.split("\n")
|
|
108
|
+
.map((l) => l.trim())
|
|
109
|
+
.filter(Boolean);
|
|
110
|
+
return lines.filter((l) => JS_FILE_RE.test(l));
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(outFile, { force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function runJsRecon(target) {
|
|
117
|
+
const outputDir = mkdtempSync(join(tmpdir(), "jsr-devbench-"));
|
|
118
|
+
try {
|
|
119
|
+
const researchOutput = join(outputDir, "research.json");
|
|
120
|
+
const jsReconArgs = [
|
|
121
|
+
jsReconEntry,
|
|
122
|
+
"lazyload",
|
|
123
|
+
"-u",
|
|
124
|
+
target,
|
|
125
|
+
"-y",
|
|
126
|
+
"--no-sandbox",
|
|
127
|
+
"-t",
|
|
128
|
+
opts.threads,
|
|
129
|
+
"--research",
|
|
130
|
+
"--research-output",
|
|
131
|
+
researchOutput,
|
|
132
|
+
"-o",
|
|
133
|
+
outputDir,
|
|
134
|
+
// Isolate the response cache per run — without this, successive benchmark runs
|
|
135
|
+
// against the same host:port (e.g. piloting Vite then webpack, both on
|
|
136
|
+
// localhost:3000) share the CWD-default .resp_cache.db and the second run silently
|
|
137
|
+
// serves the first run's cached HTML/responses instead of the new server's.
|
|
138
|
+
"--cache-file",
|
|
139
|
+
join(outputDir, ".resp_cache.db"),
|
|
140
|
+
];
|
|
141
|
+
if (opts.insecure) jsReconArgs.push("-k");
|
|
142
|
+
const result = spawnSync("node", jsReconArgs, { encoding: "utf8", maxBuffer: 50 * 1024 * 1024 });
|
|
143
|
+
if (result.error) throw new Error(`js-recon lazyload failed to run: ${result.error.message}`);
|
|
144
|
+
if (result.status !== 0) {
|
|
145
|
+
throw new Error(`js-recon lazyload exited with status ${result.status}: ${(result.stderr || "").trim()}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let urls = [];
|
|
149
|
+
if (existsSync(researchOutput)) {
|
|
150
|
+
const research = JSON.parse(readFileSync(researchOutput, "utf8"));
|
|
151
|
+
urls = Object.values(research).flat();
|
|
152
|
+
}
|
|
153
|
+
return { urls, outputDir: opts.keepOutputDirs ? outputDir : null };
|
|
154
|
+
} finally {
|
|
155
|
+
if (!opts.keepOutputDirs) rmSync(outputDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const report = { generatedAt: null, targets: [] };
|
|
160
|
+
const failedTargets = [];
|
|
161
|
+
|
|
162
|
+
for (const target of readTargets()) {
|
|
163
|
+
console.log(`\n[i] Benchmarking ${target}`);
|
|
164
|
+
let katanaUrls, jsReconResult;
|
|
165
|
+
try {
|
|
166
|
+
katanaUrls = runKatana(target);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(`[!] katana failed for ${target}: ${err.message}`);
|
|
169
|
+
failedTargets.push(target);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
jsReconResult = runJsRecon(target);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[!] js-recon lazyload failed for ${target}: ${err.message}`);
|
|
176
|
+
failedTargets.push(target);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const katanaSet = normalizeSet(katanaUrls);
|
|
181
|
+
const jsReconSet = normalizeSet(jsReconResult.urls);
|
|
182
|
+
|
|
183
|
+
const missedByJsRecon = [...katanaSet].filter((u) => !jsReconSet.has(u));
|
|
184
|
+
const foundOnlyByJsRecon = [...jsReconSet].filter((u) => !katanaSet.has(u));
|
|
185
|
+
const intersection = [...katanaSet].filter((u) => jsReconSet.has(u));
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
` katana: ${katanaSet.size} JS URL(s) | js-recon: ${jsReconSet.size} URL(s) | overlap: ${intersection.length} | js-recon missed: ${missedByJsRecon.length} | js-recon only: ${foundOnlyByJsRecon.length}`
|
|
189
|
+
);
|
|
190
|
+
if (missedByJsRecon.length > 0) {
|
|
191
|
+
console.log(" Missed by js-recon:");
|
|
192
|
+
for (const u of missedByJsRecon) console.log(` - ${u}`);
|
|
193
|
+
}
|
|
194
|
+
if (opts.keepOutputDirs && jsReconResult.outputDir) {
|
|
195
|
+
console.log(` js-recon output kept at: ${jsReconResult.outputDir}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
report.targets.push({
|
|
199
|
+
target,
|
|
200
|
+
katanaCount: katanaSet.size,
|
|
201
|
+
jsReconCount: jsReconSet.size,
|
|
202
|
+
overlapCount: intersection.length,
|
|
203
|
+
missedByJsRecon,
|
|
204
|
+
foundOnlyByJsRecon,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (opts.output) {
|
|
209
|
+
report.generatedAt = new Date().toISOString();
|
|
210
|
+
writeFileSync(opts.output, JSON.stringify(report, null, 4));
|
|
211
|
+
console.log(`\n[✓] Full report written to ${opts.output}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (failedTargets.length > 0) {
|
|
215
|
+
console.error(`\n[!] ${failedTargets.length} target(s) failed to run: ${failedTargets.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const totalMissed = report.targets.reduce((sum, t) => sum + t.missedByJsRecon.length, 0);
|
|
219
|
+
if (totalMissed > 0) {
|
|
220
|
+
console.error(`\n[!] ${totalMissed} URL(s) missed by js-recon across ${report.targets.length} target(s).`);
|
|
221
|
+
}
|
|
222
|
+
if (totalMissed > 0 || failedTargets.length > 0) {
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
console.log(`\n[✓] No discovery gaps found across ${report.targets.length} target(s).`);
|