@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/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
+ }
@@ -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
+ }