@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/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { runPipeline } from './engine.js';
|
|
5
|
+
import { createLogger } from './logger.js';
|
|
6
|
+
|
|
7
|
+
export function run() {
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('metho')
|
|
12
|
+
.description('Automated recon pipeline: subfinder → gau → filter → katana → findsomething')
|
|
13
|
+
.version('1.0.0')
|
|
14
|
+
.option('-d, --domain <domain>', 'Single target domain')
|
|
15
|
+
.option('-dL, --domain-list <file>', 'File with list of domains/subdomains')
|
|
16
|
+
.option('-o, --output <dir>', 'Output directory', './metho-results')
|
|
17
|
+
.option('--skip-subfinder', 'Skip subdomain enumeration')
|
|
18
|
+
.option('--skip-subdomain-probe', 'Skip httpx subdomain liveness check')
|
|
19
|
+
.option('--skip-gau', 'Skip passive URL crawling')
|
|
20
|
+
.option('--skip-filter', 'Skip URL filtering/liveness check')
|
|
21
|
+
.option('--skip-katana', 'Skip active crawling')
|
|
22
|
+
.option('--skip-findsomething', 'Skip secret scanning')
|
|
23
|
+
.option('--katana-chunk-size <n>', 'Max URLs per katana batch', '1000')
|
|
24
|
+
.option('--katana-depth <n>', 'Katana crawl depth', '2')
|
|
25
|
+
.option('--findsomething-path <path>', 'Path to findsomething_secrets.py')
|
|
26
|
+
.option('--debug', 'Verbose debug logging')
|
|
27
|
+
.option('--gui', 'Launch web GUI in browser')
|
|
28
|
+
.option('--port <n>', 'GUI server port', '3000');
|
|
29
|
+
|
|
30
|
+
program.parse();
|
|
31
|
+
const opts = program.opts();
|
|
32
|
+
|
|
33
|
+
// GUI mode
|
|
34
|
+
if (opts.gui) {
|
|
35
|
+
import('./server.js').then(({ startServer }) => {
|
|
36
|
+
startServer(parseInt(opts.port, 10));
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// CLI mode validation
|
|
42
|
+
if (!opts.domain && !opts.domainList) {
|
|
43
|
+
console.error('Error: Must provide either -d <domain> or -dL <file>, or use --gui for web interface');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (opts.domain && opts.domainList) {
|
|
47
|
+
console.error('Error: Cannot use both -d and -dL together');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (opts.domainList && !existsSync(opts.domainList)) {
|
|
51
|
+
console.error(`Error: Domain list file not found: ${opts.domainList}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const config = {
|
|
56
|
+
domain: opts.domain || null,
|
|
57
|
+
domainList: opts.domainList ? resolve(opts.domainList) : null,
|
|
58
|
+
outputDir: resolve(opts.output),
|
|
59
|
+
skipSubfinder: opts.skipSubfinder || false,
|
|
60
|
+
skipSubdomainProbe: opts.skipSubdomainProbe || false,
|
|
61
|
+
skipGau: opts.skipGau || false,
|
|
62
|
+
skipFilter: opts.skipFilter || false,
|
|
63
|
+
skipKatana: opts.skipKatana || false,
|
|
64
|
+
skipFindsomething: opts.skipFindsomething || false,
|
|
65
|
+
katanaChunkSize: parseInt(opts.katanaChunkSize, 10),
|
|
66
|
+
katanaDepth: parseInt(opts.katanaDepth, 10),
|
|
67
|
+
findsomethingPath: opts.findsomethingPath || '',
|
|
68
|
+
debug: opts.debug || false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const logger = createLogger(config.debug);
|
|
72
|
+
runPipeline(config, logger).catch((err) => {
|
|
73
|
+
logger.error(`Pipeline failed: ${err.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
76
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const BLOCKED_EXTENSIONS = new Set([
|
|
2
|
+
// Images
|
|
3
|
+
'png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'bmp', 'webp', 'tiff', 'tif', 'avif',
|
|
4
|
+
// Fonts
|
|
5
|
+
'ttf', 'woff', 'woff2', 'eot', 'otf',
|
|
6
|
+
// Media
|
|
7
|
+
'mp4', 'mp3', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv', 'ogg', 'wav', 'flac',
|
|
8
|
+
// Documents / archives
|
|
9
|
+
'pdf', 'zip', 'tar', 'gz', 'rar', '7z', 'bz2', 'xz',
|
|
10
|
+
// Stylesheets / maps
|
|
11
|
+
'css', 'map',
|
|
12
|
+
// Binary / misc
|
|
13
|
+
'exe', 'dll', 'so', 'dylib', 'bin', 'dmg', 'iso', 'img', 'apk', 'deb', 'rpm',
|
|
14
|
+
'swf',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_KATANA_CHUNK_SIZE = 1000;
|
|
18
|
+
export const DEFAULT_KATANA_DEPTH = 2;
|
|
19
|
+
|
|
20
|
+
export const HTTPX_STATUS_CODES = '200,201,202,204,301,302,307,308,401,403';
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, copyFileSync, readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import { setupSignalHandlers, getGlobalContext, createRunContext } from './signals.js';
|
|
4
|
+
import { ensureTools } from './tools/manager.js';
|
|
5
|
+
import { SubfinderStep } from './steps/subfinder.js';
|
|
6
|
+
import { SubdomainProbeStep } from './steps/subdomain-probe.js';
|
|
7
|
+
import { GauStep } from './steps/gau.js';
|
|
8
|
+
import { FilterStep } from './steps/filter.js';
|
|
9
|
+
import { KatanaStep } from './steps/katana.js';
|
|
10
|
+
import { FindSomethingStep } from './steps/findsomething.js';
|
|
11
|
+
|
|
12
|
+
function makeRunDir(outputDir, domain, domainList) {
|
|
13
|
+
const label = domain || basename(domainList, '.txt');
|
|
14
|
+
const now = new Date();
|
|
15
|
+
const ts = now.toISOString().replace(/[-:T]/g, '').replace(/\..+/, '').replace(/(\d{8})(\d{6})/, '$1-$2');
|
|
16
|
+
const dirName = `${label}-${ts}`;
|
|
17
|
+
const runDir = join(outputDir, dirName);
|
|
18
|
+
mkdirSync(runDir, { recursive: true });
|
|
19
|
+
return runDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run the full recon pipeline.
|
|
24
|
+
* @param {object} config - pipeline configuration
|
|
25
|
+
* @param {object} logger - logger instance
|
|
26
|
+
* @param {RunContext} [ctx] - optional run context (created automatically if not provided)
|
|
27
|
+
*/
|
|
28
|
+
export async function runPipeline(config, logger, ctx) {
|
|
29
|
+
// Use provided context (server mode) or global context (CLI mode)
|
|
30
|
+
if (!ctx) {
|
|
31
|
+
ctx = getGlobalContext();
|
|
32
|
+
setupSignalHandlers(logger);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.banner('METHO - Automated Recon Pipeline');
|
|
36
|
+
|
|
37
|
+
// Create output directory
|
|
38
|
+
const runDir = makeRunDir(config.outputDir, config.domain, config.domainList);
|
|
39
|
+
logger.info(`Output directory: ${runDir}`);
|
|
40
|
+
|
|
41
|
+
// Set up log file
|
|
42
|
+
const logFile = join(runDir, 'metho.log');
|
|
43
|
+
logger.setLogFile(logFile);
|
|
44
|
+
logger.debug(`Config: ${JSON.stringify(config, null, 2)}`);
|
|
45
|
+
|
|
46
|
+
// Prepare input file
|
|
47
|
+
const inputFile = join(runDir, '00-input-domains.txt');
|
|
48
|
+
if (config.domain) {
|
|
49
|
+
writeFileSync(inputFile, config.domain + '\n');
|
|
50
|
+
} else {
|
|
51
|
+
copyFileSync(config.domainList, inputFile);
|
|
52
|
+
}
|
|
53
|
+
ctx.trackFile(inputFile, 'Input domains');
|
|
54
|
+
logger.info(`Input: ${config.domain || config.domainList}`);
|
|
55
|
+
|
|
56
|
+
// Detect tools
|
|
57
|
+
logger.info('Checking required tools...');
|
|
58
|
+
await ensureTools(config, logger);
|
|
59
|
+
|
|
60
|
+
// Track results for final summary
|
|
61
|
+
const results = [];
|
|
62
|
+
let currentInput = inputFile;
|
|
63
|
+
let stepNum = 0;
|
|
64
|
+
|
|
65
|
+
// --- Step 1: Subfinder ---
|
|
66
|
+
stepNum++;
|
|
67
|
+
const subdomainsFile = join(runDir, '01-subdomains.txt');
|
|
68
|
+
if (config.skipSubfinder) {
|
|
69
|
+
logger.step(stepNum, 'Subfinder (subdomain enumeration)', 'skip');
|
|
70
|
+
} else {
|
|
71
|
+
logger.step(stepNum, 'Subfinder (subdomain enumeration)', 'start');
|
|
72
|
+
const subfinder = new SubfinderStep(logger, ctx);
|
|
73
|
+
const res = await subfinder.execute(config, currentInput, subdomainsFile);
|
|
74
|
+
ctx.trackFile(subdomainsFile, 'Subdomains');
|
|
75
|
+
results.push({ step: 'Subfinder', file: subdomainsFile, lines: res.lineCount });
|
|
76
|
+
logger.step(stepNum, `Subfinder → ${res.lineCount} subdomains`, 'done');
|
|
77
|
+
logger.result('Subfinder', subdomainsFile, res.lineCount);
|
|
78
|
+
currentInput = subdomainsFile;
|
|
79
|
+
}
|
|
80
|
+
if (ctx.isInterrupted()) return printSummary(results, runDir, logger);
|
|
81
|
+
|
|
82
|
+
// --- Step 2: Subdomain Probe (httpx) ---
|
|
83
|
+
stepNum++;
|
|
84
|
+
const liveSubsFile = join(runDir, '02-live-subdomains.txt');
|
|
85
|
+
if (config.skipSubdomainProbe) {
|
|
86
|
+
logger.step(stepNum, 'Subdomain Probe (httpx)', 'skip');
|
|
87
|
+
} else {
|
|
88
|
+
logger.step(stepNum, 'Subdomain Probe (httpx on subdomains)', 'start');
|
|
89
|
+
const probe = new SubdomainProbeStep(logger, ctx);
|
|
90
|
+
const res = await probe.execute(config, currentInput, liveSubsFile);
|
|
91
|
+
ctx.trackFile(liveSubsFile, 'Live Subdomains');
|
|
92
|
+
results.push({ step: 'Subdomain Probe', file: liveSubsFile, lines: res.lineCount });
|
|
93
|
+
logger.step(stepNum, `Subdomain Probe → ${res.lineCount} live subdomains`, 'done');
|
|
94
|
+
logger.result('Subdomain Probe', liveSubsFile, res.lineCount);
|
|
95
|
+
currentInput = liveSubsFile;
|
|
96
|
+
}
|
|
97
|
+
if (ctx.isInterrupted()) return printSummary(results, runDir, logger);
|
|
98
|
+
|
|
99
|
+
// --- Step 3: GAU ---
|
|
100
|
+
stepNum++;
|
|
101
|
+
const gauFile = join(runDir, '03-passive-urls.txt');
|
|
102
|
+
if (config.skipGau) {
|
|
103
|
+
logger.step(stepNum, 'GAU (passive URL crawling)', 'skip');
|
|
104
|
+
} else {
|
|
105
|
+
logger.step(stepNum, 'GAU (passive URL crawling)', 'start');
|
|
106
|
+
const gau = new GauStep(logger, ctx);
|
|
107
|
+
const res = await gau.execute(config, currentInput, gauFile);
|
|
108
|
+
ctx.trackFile(gauFile, 'Passive URLs');
|
|
109
|
+
results.push({ step: 'GAU', file: gauFile, lines: res.lineCount });
|
|
110
|
+
logger.step(stepNum, `GAU → ${res.lineCount} URLs`, 'done');
|
|
111
|
+
logger.result('GAU', gauFile, res.lineCount);
|
|
112
|
+
currentInput = gauFile;
|
|
113
|
+
}
|
|
114
|
+
if (ctx.isInterrupted()) return printSummary(results, runDir, logger);
|
|
115
|
+
|
|
116
|
+
// --- Step 4: Filter ---
|
|
117
|
+
stepNum++;
|
|
118
|
+
const liveUrlsFile = join(runDir, '04-live-urls.txt');
|
|
119
|
+
if (config.skipFilter) {
|
|
120
|
+
logger.step(stepNum, 'Filter (extension + liveness)', 'skip');
|
|
121
|
+
} else {
|
|
122
|
+
logger.step(stepNum, 'Filter (extension + liveness)', 'start');
|
|
123
|
+
const filter = new FilterStep(logger, ctx);
|
|
124
|
+
const res = await filter.execute(config, currentInput, liveUrlsFile);
|
|
125
|
+
ctx.trackFile(liveUrlsFile, 'Live URLs');
|
|
126
|
+
results.push({ step: 'Filter', file: liveUrlsFile, lines: res.lineCount });
|
|
127
|
+
logger.step(stepNum, `Filter → ${res.lineCount} live URLs`, 'done');
|
|
128
|
+
logger.result('Filter', liveUrlsFile, res.lineCount);
|
|
129
|
+
currentInput = liveUrlsFile;
|
|
130
|
+
}
|
|
131
|
+
if (ctx.isInterrupted()) return printSummary(results, runDir, logger);
|
|
132
|
+
|
|
133
|
+
// --- Step 5: Katana ---
|
|
134
|
+
stepNum++;
|
|
135
|
+
const crawledFile = join(runDir, '05-crawled-urls.txt');
|
|
136
|
+
if (config.skipKatana) {
|
|
137
|
+
logger.step(stepNum, 'Katana (active crawling)', 'skip');
|
|
138
|
+
} else {
|
|
139
|
+
logger.step(stepNum, 'Katana (active crawling)', 'start');
|
|
140
|
+
const katana = new KatanaStep(logger, ctx);
|
|
141
|
+
const res = await katana.execute(config, currentInput, crawledFile);
|
|
142
|
+
ctx.trackFile(crawledFile, 'Crawled URLs');
|
|
143
|
+
results.push({ step: 'Katana', file: crawledFile, lines: res.lineCount });
|
|
144
|
+
logger.step(stepNum, `Katana → ${res.lineCount} URLs`, 'done');
|
|
145
|
+
logger.result('Katana', crawledFile, res.lineCount);
|
|
146
|
+
currentInput = crawledFile;
|
|
147
|
+
}
|
|
148
|
+
if (ctx.isInterrupted()) return printSummary(results, runDir, logger);
|
|
149
|
+
|
|
150
|
+
// --- Step 6: FindSomething ---
|
|
151
|
+
stepNum++;
|
|
152
|
+
const secretsFile = join(runDir, '06-secrets.txt');
|
|
153
|
+
if (config.skipFindsomething) {
|
|
154
|
+
logger.step(stepNum, 'FindSomething (secret scanning)', 'skip');
|
|
155
|
+
} else {
|
|
156
|
+
logger.step(stepNum, 'FindSomething (secret scanning)', 'start');
|
|
157
|
+
const fs = new FindSomethingStep(logger, ctx);
|
|
158
|
+
const res = await fs.execute(config, currentInput, secretsFile);
|
|
159
|
+
ctx.trackFile(secretsFile, 'Secrets');
|
|
160
|
+
results.push({ step: 'FindSomething', file: secretsFile, lines: res.lineCount });
|
|
161
|
+
logger.step(stepNum, `FindSomething → ${res.lineCount} findings`, 'done');
|
|
162
|
+
logger.result('FindSomething', secretsFile, res.lineCount);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
printSummary(results, runDir, logger);
|
|
166
|
+
return { results, runDir };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function printSummary(results, runDir, logger) {
|
|
170
|
+
logger.banner('Pipeline Complete');
|
|
171
|
+
logger.info(`Results directory: ${runDir}`);
|
|
172
|
+
if (results.length > 0) {
|
|
173
|
+
logger.info('Summary:');
|
|
174
|
+
for (const r of results) {
|
|
175
|
+
logger.info(` ${r.step}: ${r.lines} lines → ${r.file}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
logger.info(`Full log: ${join(runDir, 'metho.log')}`);
|
|
179
|
+
return { results, runDir };
|
|
180
|
+
}
|