@paa1997/metho 1.0.0
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/bin/metho.js +3 -0
- package/package.json +25 -0
- package/scripts/findsomething_secrets.py +1389 -0
- package/src/cli.js +76 -0
- package/src/constants.js +20 -0
- package/src/engine.js +180 -0
- package/src/gui.html +772 -0
- package/src/logger.js +95 -0
- package/src/server.js +261 -0
- package/src/signals.js +101 -0
- package/src/steps/base-step.js +111 -0
- package/src/steps/filter.js +43 -0
- package/src/steps/findsomething.js +42 -0
- package/src/steps/gau.js +86 -0
- package/src/steps/katana.js +61 -0
- package/src/steps/subdomain-probe.js +19 -0
- package/src/steps/subfinder.js +22 -0
- package/src/tools/installer.js +111 -0
- package/src/tools/manager.js +114 -0
- package/src/tools/registry.js +45 -0
- package/src/utils/dedup.js +37 -0
- package/src/utils/file-splitter.js +48 -0
- package/src/utils/filter-extensions.js +52 -0
package/src/steps/gau.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { BaseStep } from './base-step.js';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { createReadStream, createWriteStream, unlinkSync } from 'fs';
|
|
4
|
+
import { dedup } from '../utils/dedup.js';
|
|
5
|
+
|
|
6
|
+
const isWin = process.platform === 'win32';
|
|
7
|
+
|
|
8
|
+
export class GauStep extends BaseStep {
|
|
9
|
+
constructor(logger, ctx) {
|
|
10
|
+
super('gau', logger, ctx);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async execute(config, inputFile, outputFile) {
|
|
14
|
+
const rawFile = outputFile + '.raw';
|
|
15
|
+
|
|
16
|
+
await this._runGau(inputFile, rawFile);
|
|
17
|
+
|
|
18
|
+
const rawCount = await this.countLines(rawFile);
|
|
19
|
+
this.logger.info(`GAU raw: ${rawCount} URLs, deduplicating...`);
|
|
20
|
+
|
|
21
|
+
const uniqueCount = await dedup(rawFile, outputFile);
|
|
22
|
+
this.logger.info(`GAU deduped: ${uniqueCount} unique URLs (removed ${rawCount - uniqueCount} duplicates)`);
|
|
23
|
+
|
|
24
|
+
try { unlinkSync(rawFile); } catch { /* ignore */ }
|
|
25
|
+
|
|
26
|
+
return { code: 0, lineCount: uniqueCount };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_runGau(inputFile, outputFile) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (this.ctx.isInterrupted()) return reject(new Error('Pipeline interrupted'));
|
|
32
|
+
|
|
33
|
+
const args = ['--subs'];
|
|
34
|
+
this.logger.debug(`gau: gau ${args.join(' ')} < ${inputFile} > ${outputFile}`);
|
|
35
|
+
|
|
36
|
+
const child = spawn('gau', args, {
|
|
37
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
|
+
shell: isWin,
|
|
39
|
+
});
|
|
40
|
+
this.ctx.registerChild(child);
|
|
41
|
+
|
|
42
|
+
const outStream = createWriteStream(outputFile);
|
|
43
|
+
const stdinStream = createReadStream(inputFile);
|
|
44
|
+
|
|
45
|
+
stdinStream.pipe(child.stdin);
|
|
46
|
+
stdinStream.on('error', (err) => {
|
|
47
|
+
this.logger.debug(`gau stdin error: ${err.message}`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
child.stdout.pipe(outStream);
|
|
51
|
+
|
|
52
|
+
child.stdout.on('data', (data) => {
|
|
53
|
+
this.logger.debug(`gau stdout chunk: ${data.toString().split('\n').length - 1} lines`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let stderrBuf = '';
|
|
57
|
+
child.stderr.on('data', (data) => {
|
|
58
|
+
const text = data.toString().trim();
|
|
59
|
+
if (text) {
|
|
60
|
+
stderrBuf += text + '\n';
|
|
61
|
+
this.logger.debug(`gau stderr: ${text}`);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
child.on('error', (err) => {
|
|
66
|
+
this.ctx.clearChild(child);
|
|
67
|
+
reject(new Error(`gau: Failed to spawn: ${err.message}`));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
child.on('close', async (code) => {
|
|
71
|
+
this.ctx.clearChild(child);
|
|
72
|
+
outStream.end();
|
|
73
|
+
|
|
74
|
+
if (this.ctx.isInterrupted()) return reject(new Error('Pipeline interrupted'));
|
|
75
|
+
|
|
76
|
+
const lineCount = await this.countLines(outputFile);
|
|
77
|
+
|
|
78
|
+
if (code !== 0 && lineCount === 0) {
|
|
79
|
+
return reject(new Error(`gau exited with code ${code}\n${stderrBuf}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
resolve({ code, lineCount });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseStep } from './base-step.js';
|
|
2
|
+
import { splitFile } from '../utils/file-splitter.js';
|
|
3
|
+
import { dedup } from '../utils/dedup.js';
|
|
4
|
+
import { join, dirname } from 'path';
|
|
5
|
+
import { unlinkSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
export class KatanaStep extends BaseStep {
|
|
8
|
+
constructor(logger, ctx) {
|
|
9
|
+
super('katana', logger, ctx);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async execute(config, inputFile, outputFile) {
|
|
13
|
+
const lineCount = await this.countLines(inputFile);
|
|
14
|
+
const chunkSize = config.katanaChunkSize;
|
|
15
|
+
const depth = config.katanaDepth;
|
|
16
|
+
|
|
17
|
+
if (lineCount <= chunkSize) {
|
|
18
|
+
// Small enough to run in one shot
|
|
19
|
+
this.logger.debug(`Katana: ${lineCount} URLs, running single batch`);
|
|
20
|
+
const args = ['-list', inputFile, '-d', String(depth), '-o', outputFile];
|
|
21
|
+
return this.runCommand('katana', args, { outputFile });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Split into chunks and run sequentially
|
|
25
|
+
this.logger.info(`Katana: ${lineCount} URLs, splitting into chunks of ${chunkSize}`);
|
|
26
|
+
const tmpDir = dirname(outputFile);
|
|
27
|
+
const chunks = await splitFile(inputFile, chunkSize, tmpDir, 'katana-chunk');
|
|
28
|
+
this.logger.info(`Katana: ${chunks.length} chunks to process`);
|
|
29
|
+
|
|
30
|
+
const chunkOutputs = [];
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
33
|
+
this.logger.info(`Katana chunk ${i + 1}/${chunks.length}...`);
|
|
34
|
+
const chunkOutput = join(tmpDir, `katana-out-${i}.txt`);
|
|
35
|
+
const args = ['-list', chunks[i], '-d', String(depth), '-o', chunkOutput];
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await this.runCommand('katana', args, { outputFile: chunkOutput });
|
|
39
|
+
chunkOutputs.push(chunkOutput);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
this.logger.warn(`Katana chunk ${i + 1} failed: ${err.message}`);
|
|
42
|
+
// Continue with other chunks
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Clean up chunk input
|
|
46
|
+
try { unlinkSync(chunks[i]); } catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Merge and dedup all chunk outputs
|
|
50
|
+
this.logger.debug('Katana: Merging and deduplicating chunk outputs...');
|
|
51
|
+
await dedup(chunkOutputs, outputFile);
|
|
52
|
+
|
|
53
|
+
// Clean up chunk outputs
|
|
54
|
+
for (const f of chunkOutputs) {
|
|
55
|
+
try { unlinkSync(f); } catch { /* ignore */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const finalCount = await this.countLines(outputFile);
|
|
59
|
+
return { code: 0, lineCount: finalCount };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { BaseStep } from './base-step.js';
|
|
2
|
+
import { getToolPath } from '../tools/manager.js';
|
|
3
|
+
|
|
4
|
+
export class SubdomainProbeStep extends BaseStep {
|
|
5
|
+
constructor(logger, ctx) {
|
|
6
|
+
super('subdomain-probe', logger, ctx);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async execute(config, inputFile, outputFile) {
|
|
10
|
+
const args = [
|
|
11
|
+
'-l', inputFile,
|
|
12
|
+
'-o', outputFile,
|
|
13
|
+
'-silent',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const result = await this.runCommand(getToolPath('httpx'), args, { outputFile });
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BaseStep } from './base-step.js';
|
|
2
|
+
|
|
3
|
+
export class SubfinderStep extends BaseStep {
|
|
4
|
+
constructor(logger, ctx) {
|
|
5
|
+
super('subfinder', logger, ctx);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async execute(config, inputFile, outputFile) {
|
|
9
|
+
const args = [];
|
|
10
|
+
|
|
11
|
+
if (config.domain && !config.domainList) {
|
|
12
|
+
args.push('-d', config.domain);
|
|
13
|
+
} else {
|
|
14
|
+
args.push('-dL', inputFile);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
args.push('-all', '-o', outputFile);
|
|
18
|
+
|
|
19
|
+
const result = await this.runCommand('subfinder', args, { outputFile });
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const isWin = process.platform === 'win32';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a binary is available on PATH.
|
|
9
|
+
*/
|
|
10
|
+
export function isBinaryAvailable(binary) {
|
|
11
|
+
try {
|
|
12
|
+
const cmd = isWin ? `where ${binary}` : `which ${binary}`;
|
|
13
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if httpx on PATH is ProjectDiscovery's version (not Python's httpx CLI).
|
|
22
|
+
*/
|
|
23
|
+
export function isProjectDiscoveryHttpx() {
|
|
24
|
+
try {
|
|
25
|
+
const output = execSync('httpx -version', { stdio: 'pipe', timeout: 5000 }).toString();
|
|
26
|
+
return true;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const stderr = err.stderr ? err.stderr.toString() : '';
|
|
29
|
+
const stdout = err.stdout ? err.stdout.toString() : '';
|
|
30
|
+
const combined = stdout + stderr;
|
|
31
|
+
if (combined.includes('projectdiscovery') || combined.includes('Current Version:')) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (combined.includes('[OPTIONS] URL') || combined.includes('No such option')) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempt to install a tool using its install command (globally via go install).
|
|
43
|
+
* Returns true if install succeeded, false otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export function attemptInstall(toolDef, logger) {
|
|
46
|
+
if (!toolDef.installCmd) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (toolDef.installCmd.startsWith('go install')) {
|
|
51
|
+
if (!isBinaryAvailable('go')) {
|
|
52
|
+
logger.warn('Go is not installed. Cannot auto-install Go tools.');
|
|
53
|
+
logger.warn('Install Go from https://go.dev/dl/ and ensure GOPATH/bin is in PATH.');
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.info(`Installing ${toolDef.binary}: ${toolDef.installCmd}`);
|
|
59
|
+
try {
|
|
60
|
+
execSync(toolDef.installCmd, {
|
|
61
|
+
stdio: 'pipe',
|
|
62
|
+
timeout: 120000,
|
|
63
|
+
env: { ...process.env },
|
|
64
|
+
});
|
|
65
|
+
return isBinaryAvailable(toolDef.binary);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.debug(`Install failed for ${toolDef.binary}: ${err.message}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Install a Go tool to a local directory instead of GOPATH/bin.
|
|
74
|
+
* Returns the full path to the installed binary, or null on failure.
|
|
75
|
+
*/
|
|
76
|
+
export function installToLocal(toolDef, localBinDir, logger) {
|
|
77
|
+
if (!toolDef.installCmd || !toolDef.installCmd.startsWith('go install')) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isBinaryAvailable('go')) {
|
|
82
|
+
logger.warn('Go is not installed. Cannot auto-install Go tools.');
|
|
83
|
+
logger.warn('Install Go from https://go.dev/dl/ and ensure GOPATH/bin is in PATH.');
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mkdirSync(localBinDir, { recursive: true });
|
|
88
|
+
|
|
89
|
+
const binaryName = toolDef.binary + (isWin ? '.exe' : '');
|
|
90
|
+
const binaryPath = join(localBinDir, binaryName);
|
|
91
|
+
|
|
92
|
+
logger.info(`Installing ${toolDef.binary} to ${localBinDir}...`);
|
|
93
|
+
try {
|
|
94
|
+
execSync(toolDef.installCmd, {
|
|
95
|
+
stdio: 'pipe',
|
|
96
|
+
timeout: 120000,
|
|
97
|
+
env: { ...process.env, GOBIN: localBinDir },
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (existsSync(binaryPath)) {
|
|
101
|
+
logger.debug(`${toolDef.binary} installed at ${binaryPath}`);
|
|
102
|
+
return binaryPath;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logger.debug(`Binary not found at expected path: ${binaryPath}`);
|
|
106
|
+
return null;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
logger.debug(`Local install failed for ${toolDef.binary}: ${err.message}`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { dirname, join } from 'path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { TOOLS, STEP_TOOLS } from './registry.js';
|
|
4
|
+
import { isBinaryAvailable, attemptInstall, installToLocal, isProjectDiscoveryHttpx } from './installer.js';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const LOCAL_BIN_DIR = join(__dirname, '..', '..', 'bin', 'tools');
|
|
8
|
+
|
|
9
|
+
// Resolved binary paths — maps tool name to full path (or just the name if on PATH)
|
|
10
|
+
const resolvedPaths = {};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Get the resolved binary path for a tool.
|
|
14
|
+
* Returns the local path if installed locally, otherwise the binary name (for PATH lookup).
|
|
15
|
+
*/
|
|
16
|
+
export function getToolPath(toolName) {
|
|
17
|
+
return resolvedPaths[toolName] || TOOLS[toolName]?.binary || toolName;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Detect and install all tools required by non-skipped steps.
|
|
22
|
+
* Automatically installs any missing or incorrect tools.
|
|
23
|
+
*/
|
|
24
|
+
export async function ensureTools(config, logger) {
|
|
25
|
+
const stepsToCheck = [];
|
|
26
|
+
if (!config.skipSubfinder) stepsToCheck.push('subfinder');
|
|
27
|
+
if (!config.skipGau) stepsToCheck.push('gau');
|
|
28
|
+
if (!config.skipSubdomainProbe) stepsToCheck.push('subdomainProbe');
|
|
29
|
+
if (!config.skipFilter) stepsToCheck.push('filter');
|
|
30
|
+
if (!config.skipKatana) stepsToCheck.push('katana');
|
|
31
|
+
if (!config.skipFindsomething) stepsToCheck.push('findsomething');
|
|
32
|
+
|
|
33
|
+
// Collect unique tools needed
|
|
34
|
+
const toolsNeeded = new Set();
|
|
35
|
+
for (const step of stepsToCheck) {
|
|
36
|
+
for (const tool of STEP_TOOLS[step] || []) {
|
|
37
|
+
toolsNeeded.add(tool);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const missing = [];
|
|
42
|
+
|
|
43
|
+
for (const toolName of toolsNeeded) {
|
|
44
|
+
const toolDef = TOOLS[toolName];
|
|
45
|
+
if (!toolDef) {
|
|
46
|
+
missing.push({ name: toolName, reason: 'Unknown tool (not in registry)' });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.debug(`Checking for ${toolName} (${toolDef.binary})...`);
|
|
51
|
+
|
|
52
|
+
if (isBinaryAvailable(toolDef.binary)) {
|
|
53
|
+
// Special check: verify httpx is ProjectDiscovery's, not Python's
|
|
54
|
+
if (toolName === 'httpx' && !isProjectDiscoveryHttpx()) {
|
|
55
|
+
logger.warn('Found Python\'s httpx on PATH (not ProjectDiscovery\'s)');
|
|
56
|
+
logger.info('Installing ProjectDiscovery httpx locally to avoid conflict...');
|
|
57
|
+
|
|
58
|
+
const localPath = installToLocal(toolDef, LOCAL_BIN_DIR, logger);
|
|
59
|
+
if (localPath) {
|
|
60
|
+
resolvedPaths[toolName] = localPath;
|
|
61
|
+
logger.success(`ProjectDiscovery httpx installed at ${localPath}`);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
missing.push({
|
|
66
|
+
name: toolName,
|
|
67
|
+
binary: toolDef.binary,
|
|
68
|
+
hint: 'Could not install locally. Run: ' + toolDef.installCmd,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolvedPaths[toolName] = toolDef.binary;
|
|
74
|
+
logger.debug(`${toolName} found on PATH`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Tool not found at all — try global install first
|
|
79
|
+
logger.warn(`${toolName} not found, installing...`);
|
|
80
|
+
const installed = attemptInstall(toolDef, logger);
|
|
81
|
+
|
|
82
|
+
if (installed) {
|
|
83
|
+
resolvedPaths[toolName] = toolDef.binary;
|
|
84
|
+
logger.success(`${toolName} installed successfully`);
|
|
85
|
+
} else {
|
|
86
|
+
// Try local install as fallback
|
|
87
|
+
if (toolDef.installCmd && toolDef.installCmd.startsWith('go install')) {
|
|
88
|
+
logger.info(`Global install failed, trying local install to ${LOCAL_BIN_DIR}...`);
|
|
89
|
+
const localPath = installToLocal(toolDef, LOCAL_BIN_DIR, logger);
|
|
90
|
+
if (localPath) {
|
|
91
|
+
resolvedPaths[toolName] = localPath;
|
|
92
|
+
logger.success(`${toolName} installed locally at ${localPath}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let hint = `Install manually: ${toolDef.installCmd || 'see tool documentation'}`;
|
|
98
|
+
if (toolName === 'python') {
|
|
99
|
+
hint = 'Install Python from https://python.org/downloads/';
|
|
100
|
+
}
|
|
101
|
+
missing.push({ name: toolName, binary: toolDef.binary, hint });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (missing.length > 0) {
|
|
106
|
+
logger.error('Missing required tools:');
|
|
107
|
+
for (const m of missing) {
|
|
108
|
+
logger.error(` - ${m.name} (${m.binary || '?'}): ${m.hint || m.reason}`);
|
|
109
|
+
}
|
|
110
|
+
throw new Error(`Cannot proceed: ${missing.length} required tool(s) missing. See above for install instructions.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger.success('All required tools detected');
|
|
114
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Tool definitions: binary name, install command, verify command
|
|
2
|
+
const isWin = process.platform === 'win32';
|
|
3
|
+
|
|
4
|
+
export const TOOLS = {
|
|
5
|
+
subfinder: {
|
|
6
|
+
binary: 'subfinder',
|
|
7
|
+
installCmd: 'go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest',
|
|
8
|
+
verifyCmd: 'subfinder -version',
|
|
9
|
+
description: 'Subdomain enumeration tool (ProjectDiscovery)',
|
|
10
|
+
},
|
|
11
|
+
gau: {
|
|
12
|
+
binary: 'gau',
|
|
13
|
+
installCmd: 'go install github.com/lc/gau/v2/cmd/gau@latest',
|
|
14
|
+
verifyCmd: 'gau --version',
|
|
15
|
+
description: 'Fetch known URLs from AlienVault OTX, Wayback Machine, and Common Crawl',
|
|
16
|
+
},
|
|
17
|
+
httpx: {
|
|
18
|
+
binary: 'httpx',
|
|
19
|
+
installCmd: 'go install -v github.com/projectdiscovery/httpx/cmd/httpx@latest',
|
|
20
|
+
verifyCmd: 'httpx -version',
|
|
21
|
+
description: 'HTTP probe tool (ProjectDiscovery)',
|
|
22
|
+
},
|
|
23
|
+
katana: {
|
|
24
|
+
binary: 'katana',
|
|
25
|
+
installCmd: 'go install github.com/projectdiscovery/katana/cmd/katana@latest',
|
|
26
|
+
verifyCmd: 'katana -version',
|
|
27
|
+
description: 'Active web crawler (ProjectDiscovery)',
|
|
28
|
+
},
|
|
29
|
+
python: {
|
|
30
|
+
binary: isWin ? 'python' : 'python3',
|
|
31
|
+
installCmd: null, // Cannot auto-install python
|
|
32
|
+
verifyCmd: isWin ? 'python --version' : 'python3 --version',
|
|
33
|
+
description: 'Python interpreter (for findsomething)',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Map step names to required tools
|
|
38
|
+
export const STEP_TOOLS = {
|
|
39
|
+
subfinder: ['subfinder'],
|
|
40
|
+
subdomainProbe: ['httpx'],
|
|
41
|
+
gau: ['gau'],
|
|
42
|
+
filter: ['httpx'],
|
|
43
|
+
katana: ['katana'],
|
|
44
|
+
findsomething: ['python'],
|
|
45
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Deduplicate lines from one or more input files, write unique lines to output.
|
|
6
|
+
* @param {string|string[]} inputFiles - single file path or array of file paths
|
|
7
|
+
* @param {string} outputFile - output file path
|
|
8
|
+
* @returns {Promise<number>} - number of unique lines written
|
|
9
|
+
*/
|
|
10
|
+
export async function dedup(inputFiles, outputFile) {
|
|
11
|
+
const files = Array.isArray(inputFiles) ? inputFiles : [inputFiles];
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
const output = createWriteStream(outputFile);
|
|
14
|
+
|
|
15
|
+
for (const file of files) {
|
|
16
|
+
await new Promise((resolve, reject) => {
|
|
17
|
+
const input = createReadStream(file, { encoding: 'utf-8' });
|
|
18
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
19
|
+
|
|
20
|
+
rl.on('line', (line) => {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
23
|
+
seen.add(trimmed);
|
|
24
|
+
output.write(trimmed + '\n');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
rl.on('close', resolve);
|
|
29
|
+
rl.on('error', reject);
|
|
30
|
+
input.on('error', reject);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
output.end(() => resolve(seen.size));
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Split a file into chunks of N lines each.
|
|
7
|
+
* Returns an array of chunk file paths.
|
|
8
|
+
*/
|
|
9
|
+
export async function splitFile(inputFile, chunkSize, outputDir, prefix = 'chunk') {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const input = createReadStream(inputFile, { encoding: 'utf-8' });
|
|
12
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
13
|
+
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let currentChunk = null;
|
|
16
|
+
let lineCount = 0;
|
|
17
|
+
let chunkIndex = 0;
|
|
18
|
+
|
|
19
|
+
function newChunk() {
|
|
20
|
+
if (currentChunk) currentChunk.end();
|
|
21
|
+
const chunkPath = join(outputDir, `${prefix}-${chunkIndex}.txt`);
|
|
22
|
+
chunks.push(chunkPath);
|
|
23
|
+
currentChunk = createWriteStream(chunkPath);
|
|
24
|
+
chunkIndex++;
|
|
25
|
+
lineCount = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
rl.on('line', (line) => {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed) return;
|
|
31
|
+
|
|
32
|
+
if (lineCount === 0 || lineCount >= chunkSize) {
|
|
33
|
+
newChunk();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
currentChunk.write(trimmed + '\n');
|
|
37
|
+
lineCount++;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
rl.on('close', () => {
|
|
41
|
+
if (currentChunk) currentChunk.end();
|
|
42
|
+
resolve(chunks);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
rl.on('error', reject);
|
|
46
|
+
input.on('error', reject);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createReadStream, createWriteStream } from 'fs';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { BLOCKED_EXTENSIONS } from '../constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Filter URLs by extension - removes URLs ending in blocked extensions.
|
|
7
|
+
* Returns { kept, removed } counts.
|
|
8
|
+
*/
|
|
9
|
+
export async function filterExtensions(inputFile, outputFile) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const input = createReadStream(inputFile, { encoding: 'utf-8' });
|
|
12
|
+
const output = createWriteStream(outputFile);
|
|
13
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
14
|
+
|
|
15
|
+
let kept = 0;
|
|
16
|
+
let removed = 0;
|
|
17
|
+
|
|
18
|
+
rl.on('line', (line) => {
|
|
19
|
+
const url = line.trim();
|
|
20
|
+
if (!url) return;
|
|
21
|
+
|
|
22
|
+
if (hasBlockedExtension(url)) {
|
|
23
|
+
removed++;
|
|
24
|
+
} else {
|
|
25
|
+
output.write(url + '\n');
|
|
26
|
+
kept++;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
rl.on('close', () => {
|
|
31
|
+
output.end(() => resolve({ kept, removed }));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
rl.on('error', reject);
|
|
35
|
+
input.on('error', reject);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasBlockedExtension(url) {
|
|
40
|
+
try {
|
|
41
|
+
// Strip query string and fragment
|
|
42
|
+
let path = url.split('?')[0].split('#')[0];
|
|
43
|
+
// Get the last path segment
|
|
44
|
+
const lastSegment = path.split('/').pop() || '';
|
|
45
|
+
const dotIndex = lastSegment.lastIndexOf('.');
|
|
46
|
+
if (dotIndex === -1) return false;
|
|
47
|
+
const ext = lastSegment.slice(dotIndex + 1).toLowerCase();
|
|
48
|
+
return BLOCKED_EXTENSIONS.has(ext);
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|