@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/gui.html
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Metho - Recon Pipeline</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
11
|
+
background: #0f0f1a;
|
|
12
|
+
color: #e0e0e0;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
}
|
|
15
|
+
header {
|
|
16
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
17
|
+
border-bottom: 1px solid #2a2a4a;
|
|
18
|
+
padding: 16px 24px;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 16px;
|
|
22
|
+
}
|
|
23
|
+
header h1 {
|
|
24
|
+
font-size: 22px;
|
|
25
|
+
font-weight: 700;
|
|
26
|
+
background: linear-gradient(90deg, #a78bfa, #7c3aed);
|
|
27
|
+
-webkit-background-clip: text;
|
|
28
|
+
-webkit-text-fill-color: transparent;
|
|
29
|
+
letter-spacing: 2px;
|
|
30
|
+
}
|
|
31
|
+
header .tagline { color: #888; font-size: 13px; }
|
|
32
|
+
.container { padding: 20px 24px; max-width: 1400px; margin: 0 auto; }
|
|
33
|
+
|
|
34
|
+
.card {
|
|
35
|
+
background: #1a1a2e;
|
|
36
|
+
border: 1px solid #2a2a4a;
|
|
37
|
+
border-radius: 10px;
|
|
38
|
+
padding: 20px 24px;
|
|
39
|
+
margin-bottom: 20px;
|
|
40
|
+
}
|
|
41
|
+
.card h2 {
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 1px;
|
|
45
|
+
color: #a78bfa;
|
|
46
|
+
margin-bottom: 16px;
|
|
47
|
+
}
|
|
48
|
+
.config-grid {
|
|
49
|
+
display: grid;
|
|
50
|
+
grid-template-columns: 1fr 1fr;
|
|
51
|
+
gap: 16px 32px;
|
|
52
|
+
}
|
|
53
|
+
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
|
54
|
+
.form-group label {
|
|
55
|
+
font-size: 12px; font-weight: 600; color: #999;
|
|
56
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
57
|
+
}
|
|
58
|
+
.form-group input[type="text"],
|
|
59
|
+
.form-group input[type="number"] {
|
|
60
|
+
background: #12121f; border: 1px solid #2a2a4a; border-radius: 6px;
|
|
61
|
+
padding: 9px 12px; color: #e0e0e0; font-size: 14px;
|
|
62
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
63
|
+
outline: none; transition: border-color 0.2s;
|
|
64
|
+
}
|
|
65
|
+
.form-group input:focus { border-color: #7c3aed; }
|
|
66
|
+
.form-group input::placeholder { color: #555; }
|
|
67
|
+
.skip-checks { display: flex; flex-wrap: wrap; gap: 10px 20px; margin-top: 4px; }
|
|
68
|
+
.skip-checks label {
|
|
69
|
+
display: flex; align-items: center; gap: 6px;
|
|
70
|
+
font-size: 13px; color: #bbb; cursor: pointer;
|
|
71
|
+
text-transform: none; letter-spacing: 0; font-weight: 400;
|
|
72
|
+
}
|
|
73
|
+
.skip-checks input[type="checkbox"] { accent-color: #7c3aed; width: 15px; height: 15px; }
|
|
74
|
+
.actions { display: flex; gap: 12px; margin-top: 18px; align-items: center; }
|
|
75
|
+
.btn {
|
|
76
|
+
padding: 10px 28px; border: none; border-radius: 6px;
|
|
77
|
+
font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s;
|
|
78
|
+
}
|
|
79
|
+
.btn-run { background: linear-gradient(135deg, #7c3aed, #a78bfa); color: #fff; }
|
|
80
|
+
.btn-run:hover { opacity: 0.9; transform: translateY(-1px); }
|
|
81
|
+
.btn-run:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
82
|
+
.btn-stop { background: #dc2626; color: #fff; }
|
|
83
|
+
.btn-stop:hover { background: #ef4444; }
|
|
84
|
+
.btn-stop:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
85
|
+
|
|
86
|
+
/* Run tabs bar */
|
|
87
|
+
.run-tabs-bar {
|
|
88
|
+
display: flex; align-items: center; gap: 0;
|
|
89
|
+
background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 10px 10px 0 0;
|
|
90
|
+
padding: 0; overflow-x: auto; min-height: 42px;
|
|
91
|
+
}
|
|
92
|
+
.run-tab {
|
|
93
|
+
padding: 10px 20px; font-size: 13px; font-weight: 600;
|
|
94
|
+
color: #666; cursor: pointer; border: none; background: none;
|
|
95
|
+
border-bottom: 2px solid transparent; transition: all 0.2s;
|
|
96
|
+
white-space: nowrap; display: flex; align-items: center; gap: 8px;
|
|
97
|
+
}
|
|
98
|
+
.run-tab:hover { color: #aaa; background: #12121f; }
|
|
99
|
+
.run-tab.active { color: #a78bfa; border-bottom-color: #7c3aed; background: #12121f; }
|
|
100
|
+
.run-tab .run-status {
|
|
101
|
+
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
|
|
102
|
+
}
|
|
103
|
+
.run-tab .run-status.running { background: #7c3aed; animation: dot-pulse 1.5s infinite; }
|
|
104
|
+
.run-tab .run-status.done { background: #22c55e; }
|
|
105
|
+
.run-tab .run-status.error { background: #ef4444; }
|
|
106
|
+
.run-tab .run-status.stopped { background: #f59e0b; }
|
|
107
|
+
.run-tab .close-tab {
|
|
108
|
+
font-size: 14px; color: #555; cursor: pointer; margin-left: 4px;
|
|
109
|
+
line-height: 1; padding: 0 2px;
|
|
110
|
+
}
|
|
111
|
+
.run-tab .close-tab:hover { color: #ef4444; }
|
|
112
|
+
@keyframes dot-pulse {
|
|
113
|
+
0%, 100% { opacity: 1; }
|
|
114
|
+
50% { opacity: 0.3; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.no-runs-placeholder {
|
|
118
|
+
background: #1a1a2e; border: 1px solid #2a2a4a; border-radius: 10px;
|
|
119
|
+
padding: 60px 20px; text-align: center; color: #555; font-size: 14px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Run panel (per-run content) */
|
|
123
|
+
.run-panel {
|
|
124
|
+
display: none;
|
|
125
|
+
background: #1a1a2e; border: 1px solid #2a2a4a; border-top: none;
|
|
126
|
+
border-radius: 0 0 10px 10px; padding: 20px;
|
|
127
|
+
margin-bottom: 20px;
|
|
128
|
+
}
|
|
129
|
+
.run-panel.active { display: block; }
|
|
130
|
+
|
|
131
|
+
/* Pipeline steps */
|
|
132
|
+
.pipeline-steps { display: flex; align-items: center; overflow-x: auto; margin-bottom: 16px; }
|
|
133
|
+
.p-step {
|
|
134
|
+
display: flex; align-items: center; gap: 10px;
|
|
135
|
+
padding: 10px 16px; border-radius: 8px;
|
|
136
|
+
background: #12121f; border: 1px solid #2a2a4a;
|
|
137
|
+
min-width: 160px; transition: all 0.3s; flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
.p-step .num {
|
|
140
|
+
width: 28px; height: 28px; border-radius: 50%;
|
|
141
|
+
display: flex; align-items: center; justify-content: center;
|
|
142
|
+
font-size: 12px; font-weight: 700; background: #2a2a4a; color: #888; flex-shrink: 0;
|
|
143
|
+
}
|
|
144
|
+
.p-step .info { display: flex; flex-direction: column; gap: 2px; }
|
|
145
|
+
.p-step .name { font-size: 13px; font-weight: 600; }
|
|
146
|
+
.p-step .status { font-size: 11px; color: #666; }
|
|
147
|
+
.p-step.running { border-color: #7c3aed; background: #1e1a3e; }
|
|
148
|
+
.p-step.running .num { background: #7c3aed; color: #fff; animation: pulse 1.5s infinite; }
|
|
149
|
+
.p-step.running .status { color: #a78bfa; }
|
|
150
|
+
.p-step.done { border-color: #22c55e; }
|
|
151
|
+
.p-step.done .num { background: #22c55e; color: #fff; }
|
|
152
|
+
.p-step.done .status { color: #22c55e; }
|
|
153
|
+
.p-step.skipped { opacity: 0.5; }
|
|
154
|
+
.p-step.skipped .num { background: #555; color: #999; }
|
|
155
|
+
.p-step.error { border-color: #dc2626; }
|
|
156
|
+
.p-step.error .num { background: #dc2626; color: #fff; }
|
|
157
|
+
.p-arrow { color: #444; font-size: 18px; margin: 0 6px; flex-shrink: 0; }
|
|
158
|
+
@keyframes pulse {
|
|
159
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(124, 58, 237, 0.4); }
|
|
160
|
+
50% { box-shadow: 0 0 0 8px rgba(124, 58, 237, 0); }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* Bottom panels */
|
|
164
|
+
.bottom-panels { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
165
|
+
.panel {
|
|
166
|
+
background: #12121f; border: 1px solid #2a2a4a; border-radius: 10px;
|
|
167
|
+
display: flex; flex-direction: column; min-height: 350px; max-height: 500px;
|
|
168
|
+
}
|
|
169
|
+
.panel-header {
|
|
170
|
+
padding: 12px 16px; border-bottom: 1px solid #2a2a4a;
|
|
171
|
+
font-size: 13px; font-weight: 600; color: #a78bfa;
|
|
172
|
+
text-transform: uppercase; letter-spacing: 1px;
|
|
173
|
+
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
|
|
174
|
+
}
|
|
175
|
+
.panel-header .count {
|
|
176
|
+
background: #2a2a4a; padding: 2px 8px; border-radius: 10px;
|
|
177
|
+
font-size: 11px; color: #888;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* Log panel */
|
|
181
|
+
.log-panel {
|
|
182
|
+
flex: 1; overflow-y: auto; padding: 12px 16px;
|
|
183
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
184
|
+
font-size: 12px; line-height: 1.6;
|
|
185
|
+
}
|
|
186
|
+
.log-panel::-webkit-scrollbar { width: 6px; }
|
|
187
|
+
.log-panel::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
|
|
188
|
+
.log-line { white-space: pre-wrap; word-break: break-all; }
|
|
189
|
+
.log-info { color: #60a5fa; }
|
|
190
|
+
.log-debug { color: #555; }
|
|
191
|
+
.log-success { color: #22c55e; }
|
|
192
|
+
.log-error { color: #ef4444; }
|
|
193
|
+
.log-warn { color: #f59e0b; }
|
|
194
|
+
.log-banner { color: #a78bfa; font-weight: 700; }
|
|
195
|
+
|
|
196
|
+
/* Results panel with tabs */
|
|
197
|
+
.result-tabs {
|
|
198
|
+
display: flex; gap: 0; border-bottom: 1px solid #2a2a4a; flex-shrink: 0;
|
|
199
|
+
overflow-x: auto;
|
|
200
|
+
}
|
|
201
|
+
.result-tab {
|
|
202
|
+
padding: 8px 16px; font-size: 12px; font-weight: 600;
|
|
203
|
+
color: #666; cursor: pointer; border-bottom: 2px solid transparent;
|
|
204
|
+
transition: all 0.2s; white-space: nowrap; background: none; border-top: none;
|
|
205
|
+
border-left: none; border-right: none;
|
|
206
|
+
}
|
|
207
|
+
.result-tab:hover { color: #aaa; }
|
|
208
|
+
.result-tab.active { color: #a78bfa; border-bottom-color: #7c3aed; }
|
|
209
|
+
.result-tab .tab-count {
|
|
210
|
+
background: #2a2a4a; padding: 1px 6px; border-radius: 8px;
|
|
211
|
+
font-size: 10px; margin-left: 6px; color: #888;
|
|
212
|
+
}
|
|
213
|
+
.result-tab.active .tab-count { background: #7c3aed; color: #fff; }
|
|
214
|
+
|
|
215
|
+
.result-content {
|
|
216
|
+
flex: 1; overflow-y: auto; padding: 12px 16px;
|
|
217
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
218
|
+
font-size: 12px; line-height: 1.5;
|
|
219
|
+
}
|
|
220
|
+
.result-content::-webkit-scrollbar { width: 6px; }
|
|
221
|
+
.result-content::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
|
|
222
|
+
.tab-pane { display: none; }
|
|
223
|
+
.tab-pane.active { display: block; }
|
|
224
|
+
|
|
225
|
+
.result-toolbar {
|
|
226
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
227
|
+
padding: 8px 12px; background: #1a1a2e; border-radius: 6px; margin-bottom: 8px;
|
|
228
|
+
font-size: 12px; color: #888;
|
|
229
|
+
}
|
|
230
|
+
.toolbar-btns { display: flex; gap: 8px; }
|
|
231
|
+
.tbtn {
|
|
232
|
+
background: #2a2a4a; color: #ccc; border: 1px solid #3a3a5a; border-radius: 4px;
|
|
233
|
+
padding: 4px 12px; font-size: 11px; font-weight: 600; cursor: pointer;
|
|
234
|
+
transition: all 0.15s;
|
|
235
|
+
}
|
|
236
|
+
.tbtn:hover { background: #7c3aed; color: #fff; border-color: #7c3aed; }
|
|
237
|
+
|
|
238
|
+
.no-results { color: #555; text-align: center; padding: 40px 0; font-size: 13px; }
|
|
239
|
+
.result-line { color: #ccc; padding: 1px 0; }
|
|
240
|
+
.result-summary {
|
|
241
|
+
padding: 8px 12px; background: #1a1a2e; border-radius: 6px;
|
|
242
|
+
margin-bottom: 8px; color: #888; font-size: 11px;
|
|
243
|
+
}
|
|
244
|
+
.result-summary strong { color: #a78bfa; }
|
|
245
|
+
.run-dir-bar {
|
|
246
|
+
padding: 8px 16px; background: #1a1a2e; border-top: 1px solid #2a2a4a;
|
|
247
|
+
font-size: 11px; color: #666; flex-shrink: 0;
|
|
248
|
+
font-family: 'Cascadia Code', 'Fira Code', monospace;
|
|
249
|
+
}
|
|
250
|
+
.run-dir-bar strong { color: #a78bfa; }
|
|
251
|
+
</style>
|
|
252
|
+
</head>
|
|
253
|
+
<body>
|
|
254
|
+
<header>
|
|
255
|
+
<h1>METHO</h1>
|
|
256
|
+
<span class="tagline">Automated Recon Pipeline</span>
|
|
257
|
+
</header>
|
|
258
|
+
<div class="container">
|
|
259
|
+
<div class="card">
|
|
260
|
+
<h2>Configuration</h2>
|
|
261
|
+
<div class="config-grid">
|
|
262
|
+
<div class="form-group">
|
|
263
|
+
<label>Target Domain</label>
|
|
264
|
+
<input type="text" id="domain" placeholder="example.com">
|
|
265
|
+
</div>
|
|
266
|
+
<div class="form-group">
|
|
267
|
+
<label>Domain List (file path)</label>
|
|
268
|
+
<input type="text" id="domainList" placeholder="C:\path\to\domains.txt">
|
|
269
|
+
</div>
|
|
270
|
+
<div class="form-group">
|
|
271
|
+
<label>Output Directory</label>
|
|
272
|
+
<input type="text" id="outputDir" placeholder="./metho-results" value="./metho-results">
|
|
273
|
+
</div>
|
|
274
|
+
<div class="form-group">
|
|
275
|
+
<label>FindSomething Script (optional override)</label>
|
|
276
|
+
<input type="text" id="findsomethingPath" placeholder="Uses bundled script by default">
|
|
277
|
+
</div>
|
|
278
|
+
<div class="form-group">
|
|
279
|
+
<label>Katana Depth</label>
|
|
280
|
+
<input type="number" id="katanaDepth" value="2" min="1" max="10">
|
|
281
|
+
</div>
|
|
282
|
+
<div class="form-group">
|
|
283
|
+
<label>Katana Chunk Size</label>
|
|
284
|
+
<input type="number" id="katanaChunkSize" value="1000" min="100">
|
|
285
|
+
</div>
|
|
286
|
+
<div class="form-group" style="grid-column: span 2;">
|
|
287
|
+
<label>Skip Steps</label>
|
|
288
|
+
<div class="skip-checks">
|
|
289
|
+
<label><input type="checkbox" id="skipSubfinder"> Subfinder</label>
|
|
290
|
+
<label><input type="checkbox" id="skipSubdomainProbe"> Subdomain Probe</label>
|
|
291
|
+
<label><input type="checkbox" id="skipGau"> GAU</label>
|
|
292
|
+
<label><input type="checkbox" id="skipFilter"> Filter / httpx</label>
|
|
293
|
+
<label><input type="checkbox" id="skipKatana"> Katana</label>
|
|
294
|
+
<label><input type="checkbox" id="skipFindsomething"> FindSomething</label>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="actions">
|
|
299
|
+
<button class="btn btn-run" id="btn-run" onclick="startPipeline()">Run</button>
|
|
300
|
+
<button class="btn btn-stop" id="btn-stop-all" onclick="stopAllRuns()" disabled>Stop All</button>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
<!-- Run tabs + panels -->
|
|
305
|
+
<div id="run-tabs-bar" class="run-tabs-bar" style="display:none;"></div>
|
|
306
|
+
<div id="run-panels"></div>
|
|
307
|
+
<div id="no-runs" class="no-runs-placeholder">Run the pipeline to see results here</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<!-- Template for a run panel (cloned per run) -->
|
|
311
|
+
<template id="run-panel-template">
|
|
312
|
+
<div class="run-panel">
|
|
313
|
+
<div class="pipeline-steps">
|
|
314
|
+
<div class="p-step" data-step="1"><div class="num">1</div><div class="info"><div class="name">Subfinder</div><div class="status">Pending</div></div></div>
|
|
315
|
+
<div class="p-arrow">→</div>
|
|
316
|
+
<div class="p-step" data-step="2"><div class="num">2</div><div class="info"><div class="name">Probe</div><div class="status">Pending</div></div></div>
|
|
317
|
+
<div class="p-arrow">→</div>
|
|
318
|
+
<div class="p-step" data-step="3"><div class="num">3</div><div class="info"><div class="name">GAU</div><div class="status">Pending</div></div></div>
|
|
319
|
+
<div class="p-arrow">→</div>
|
|
320
|
+
<div class="p-step" data-step="4"><div class="num">4</div><div class="info"><div class="name">Filter</div><div class="status">Pending</div></div></div>
|
|
321
|
+
<div class="p-arrow">→</div>
|
|
322
|
+
<div class="p-step" data-step="5"><div class="num">5</div><div class="info"><div class="name">Katana</div><div class="status">Pending</div></div></div>
|
|
323
|
+
<div class="p-arrow">→</div>
|
|
324
|
+
<div class="p-step" data-step="6"><div class="num">6</div><div class="info"><div class="name">FindSomething</div><div class="status">Pending</div></div></div>
|
|
325
|
+
</div>
|
|
326
|
+
<div class="bottom-panels">
|
|
327
|
+
<div class="panel">
|
|
328
|
+
<div class="panel-header">Live Log <span class="count log-count">0 lines</span></div>
|
|
329
|
+
<div class="log-panel"></div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="panel">
|
|
332
|
+
<div class="panel-header">Results <span class="count results-count">-</span></div>
|
|
333
|
+
<div class="result-tabs">
|
|
334
|
+
<button class="result-tab active" data-tab="overview">Overview</button>
|
|
335
|
+
</div>
|
|
336
|
+
<div class="result-content">
|
|
337
|
+
<div class="tab-pane active" data-pane="overview">
|
|
338
|
+
<div class="no-results">Running...</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="run-dir-bar" style="display:none;"></div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</template>
|
|
346
|
+
|
|
347
|
+
<script>
|
|
348
|
+
let eventSource = null;
|
|
349
|
+
let sseConnected = false;
|
|
350
|
+
let activeRunId = null; // currently viewed tab
|
|
351
|
+
|
|
352
|
+
// Per-run state: runId → { label, status, logLines, resultCount, el, tabEl }
|
|
353
|
+
const runs = {};
|
|
354
|
+
|
|
355
|
+
const STEP_MAP = { 'subfinder': 1, 'subdomain probe': 2, 'probe': 2, 'gau': 3, 'filter': 4, 'katana': 5, 'findsomething': 6 };
|
|
356
|
+
const tabFileData = {};
|
|
357
|
+
|
|
358
|
+
function getStepNum(name) {
|
|
359
|
+
const lower = name.toLowerCase();
|
|
360
|
+
for (const [key, num] of Object.entries(STEP_MAP)) {
|
|
361
|
+
if (lower.includes(key)) return num;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function esc(str) {
|
|
367
|
+
const d = document.createElement('div');
|
|
368
|
+
d.textContent = str;
|
|
369
|
+
return d.innerHTML;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ---- SSE Connection ----
|
|
373
|
+
|
|
374
|
+
function ensureSSE() {
|
|
375
|
+
if (eventSource && sseConnected) return Promise.resolve();
|
|
376
|
+
return new Promise((resolve, reject) => {
|
|
377
|
+
if (eventSource) { eventSource.close(); eventSource = null; }
|
|
378
|
+
sseConnected = false;
|
|
379
|
+
|
|
380
|
+
const es = new EventSource('/events');
|
|
381
|
+
const timeout = setTimeout(() => { es.close(); reject(new Error('SSE timeout')); }, 5000);
|
|
382
|
+
|
|
383
|
+
es.onmessage = (e) => {
|
|
384
|
+
try {
|
|
385
|
+
const data = JSON.parse(e.data);
|
|
386
|
+
if (data.type === 'connected' && !sseConnected) {
|
|
387
|
+
sseConnected = true;
|
|
388
|
+
clearTimeout(timeout);
|
|
389
|
+
eventSource = es;
|
|
390
|
+
resolve();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
handleEvent(data);
|
|
394
|
+
} catch { /* ignore */ }
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
es.onerror = () => {
|
|
398
|
+
if (!sseConnected) {
|
|
399
|
+
clearTimeout(timeout);
|
|
400
|
+
es.close();
|
|
401
|
+
reject(new Error('SSE connection failed'));
|
|
402
|
+
} else {
|
|
403
|
+
sseConnected = false;
|
|
404
|
+
eventSource = null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ---- Start / Stop ----
|
|
411
|
+
|
|
412
|
+
async function startPipeline() {
|
|
413
|
+
const domain = document.getElementById('domain').value.trim();
|
|
414
|
+
const domainList = document.getElementById('domainList').value.trim();
|
|
415
|
+
if (!domain && !domainList) { alert('Enter a domain or domain list file path'); return; }
|
|
416
|
+
if (domain && domainList) { alert('Use either domain or domain list, not both'); return; }
|
|
417
|
+
|
|
418
|
+
const config = {
|
|
419
|
+
domain: domain || null,
|
|
420
|
+
domainList: domainList || null,
|
|
421
|
+
outputDir: document.getElementById('outputDir').value.trim() || './metho-results',
|
|
422
|
+
findsomethingPath: document.getElementById('findsomethingPath').value.trim() || '',
|
|
423
|
+
katanaDepth: parseInt(document.getElementById('katanaDepth').value) || 2,
|
|
424
|
+
katanaChunkSize: parseInt(document.getElementById('katanaChunkSize').value) || 1000,
|
|
425
|
+
skipSubfinder: document.getElementById('skipSubfinder').checked,
|
|
426
|
+
skipSubdomainProbe: document.getElementById('skipSubdomainProbe').checked,
|
|
427
|
+
skipGau: document.getElementById('skipGau').checked,
|
|
428
|
+
skipFilter: document.getElementById('skipFilter').checked,
|
|
429
|
+
skipKatana: document.getElementById('skipKatana').checked,
|
|
430
|
+
skipFindsomething: document.getElementById('skipFindsomething').checked,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
await ensureSSE();
|
|
435
|
+
} catch (err) {
|
|
436
|
+
alert('Cannot connect to server: ' + err.message);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const resp = await fetch('/run', {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
headers: { 'Content-Type': 'application/json' },
|
|
444
|
+
body: JSON.stringify(config),
|
|
445
|
+
});
|
|
446
|
+
const data = await resp.json();
|
|
447
|
+
if (data.error) { alert(data.error); return; }
|
|
448
|
+
// Run tab will be created when we receive 'run-started' event
|
|
449
|
+
} catch (err) {
|
|
450
|
+
alert('Failed to start: ' + err.message);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function stopAllRuns() {
|
|
455
|
+
fetch('/stop', { method: 'POST', body: '{}' }).catch(() => {});
|
|
456
|
+
for (const runId of Object.keys(runs)) {
|
|
457
|
+
if (runs[runId].status === 'running') {
|
|
458
|
+
addRunLog(runId, 'warn', 'Stop all requested...');
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---- Run Tab Management ----
|
|
464
|
+
|
|
465
|
+
function createRunTab(runId, label) {
|
|
466
|
+
document.getElementById('no-runs').style.display = 'none';
|
|
467
|
+
document.getElementById('run-tabs-bar').style.display = 'flex';
|
|
468
|
+
|
|
469
|
+
// Create tab button
|
|
470
|
+
const tabEl = document.createElement('button');
|
|
471
|
+
tabEl.className = 'run-tab';
|
|
472
|
+
tabEl.setAttribute('data-run', runId);
|
|
473
|
+
tabEl.innerHTML =
|
|
474
|
+
'<span class="run-status running"></span>' +
|
|
475
|
+
'<span class="run-label">' + esc(label) + '</span>' +
|
|
476
|
+
'<span class="close-tab" title="Close tab">×</span>';
|
|
477
|
+
tabEl.addEventListener('click', (e) => {
|
|
478
|
+
if (e.target.classList.contains('close-tab')) {
|
|
479
|
+
closeRunTab(runId);
|
|
480
|
+
} else {
|
|
481
|
+
switchRunTab(runId);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
document.getElementById('run-tabs-bar').appendChild(tabEl);
|
|
485
|
+
|
|
486
|
+
// Clone panel template
|
|
487
|
+
const tpl = document.getElementById('run-panel-template');
|
|
488
|
+
const panel = tpl.content.cloneNode(true).querySelector('.run-panel');
|
|
489
|
+
panel.setAttribute('data-run', runId);
|
|
490
|
+
|
|
491
|
+
// Wire up result tab clicking within this panel
|
|
492
|
+
const overviewBtn = panel.querySelector('.result-tab');
|
|
493
|
+
overviewBtn.addEventListener('click', () => switchResultTab(runId, 'overview'));
|
|
494
|
+
|
|
495
|
+
document.getElementById('run-panels').appendChild(panel);
|
|
496
|
+
|
|
497
|
+
// Track state
|
|
498
|
+
runs[runId] = { label, status: 'running', logLines: 0, resultCount: 0, el: panel, tabEl };
|
|
499
|
+
|
|
500
|
+
// Switch to this tab
|
|
501
|
+
switchRunTab(runId);
|
|
502
|
+
updateButtons();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function switchRunTab(runId) {
|
|
506
|
+
activeRunId = runId;
|
|
507
|
+
// Update tab buttons
|
|
508
|
+
document.querySelectorAll('.run-tab').forEach(t => t.classList.remove('active'));
|
|
509
|
+
const tab = document.querySelector('.run-tab[data-run="' + runId + '"]');
|
|
510
|
+
if (tab) tab.classList.add('active');
|
|
511
|
+
|
|
512
|
+
// Show/hide panels
|
|
513
|
+
document.querySelectorAll('.run-panel').forEach(p => p.classList.remove('active'));
|
|
514
|
+
const panel = document.querySelector('.run-panel[data-run="' + runId + '"]');
|
|
515
|
+
if (panel) panel.classList.add('active');
|
|
516
|
+
|
|
517
|
+
updateButtons();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function closeRunTab(runId) {
|
|
521
|
+
const run = runs[runId];
|
|
522
|
+
if (!run) return;
|
|
523
|
+
|
|
524
|
+
// Stop the run first if still running
|
|
525
|
+
if (run.status === 'running') {
|
|
526
|
+
fetch('/stop', {
|
|
527
|
+
method: 'POST',
|
|
528
|
+
headers: { 'Content-Type': 'application/json' },
|
|
529
|
+
body: JSON.stringify({ runId }),
|
|
530
|
+
}).catch(() => {});
|
|
531
|
+
addRunLog(runId, 'warn', 'Stopping...');
|
|
532
|
+
// Wait for run-ended event to actually remove, just mark it visually for now
|
|
533
|
+
setRunStatus(runId, 'stopped');
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
run.tabEl.remove();
|
|
538
|
+
run.el.remove();
|
|
539
|
+
delete runs[runId];
|
|
540
|
+
|
|
541
|
+
// Switch to another tab or show placeholder
|
|
542
|
+
const remaining = Object.keys(runs);
|
|
543
|
+
if (remaining.length > 0) {
|
|
544
|
+
switchRunTab(remaining[remaining.length - 1]);
|
|
545
|
+
} else {
|
|
546
|
+
activeRunId = null;
|
|
547
|
+
document.getElementById('run-tabs-bar').style.display = 'none';
|
|
548
|
+
document.getElementById('no-runs').style.display = 'block';
|
|
549
|
+
updateButtons();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function setRunStatus(runId, status) {
|
|
554
|
+
const run = runs[runId];
|
|
555
|
+
if (!run) return;
|
|
556
|
+
run.status = status;
|
|
557
|
+
const dot = run.tabEl.querySelector('.run-status');
|
|
558
|
+
dot.className = 'run-status ' + status;
|
|
559
|
+
updateButtons();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function updateButtons() {
|
|
563
|
+
const hasRunning = Object.values(runs).some(r => r.status === 'running');
|
|
564
|
+
document.getElementById('btn-stop-all').disabled = !hasRunning;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---- Event Handling ----
|
|
568
|
+
|
|
569
|
+
function handleEvent(data) {
|
|
570
|
+
const runId = data.runId;
|
|
571
|
+
|
|
572
|
+
switch (data.type) {
|
|
573
|
+
case 'run-started':
|
|
574
|
+
createRunTab(data.runId, data.label);
|
|
575
|
+
addRunLog(data.runId, 'info', 'Pipeline started for ' + data.label);
|
|
576
|
+
break;
|
|
577
|
+
case 'log':
|
|
578
|
+
addRunLog(runId, data.level, data.message);
|
|
579
|
+
break;
|
|
580
|
+
case 'step':
|
|
581
|
+
handleStep(runId, data);
|
|
582
|
+
break;
|
|
583
|
+
case 'banner':
|
|
584
|
+
addRunLog(runId, 'banner', '═══ ' + data.message + ' ═══');
|
|
585
|
+
break;
|
|
586
|
+
case 'result':
|
|
587
|
+
handleResult(runId, data);
|
|
588
|
+
break;
|
|
589
|
+
case 'complete':
|
|
590
|
+
handleComplete(runId, data);
|
|
591
|
+
break;
|
|
592
|
+
case 'error':
|
|
593
|
+
addRunLog(runId, 'error', data.message);
|
|
594
|
+
setRunStatus(runId, 'error');
|
|
595
|
+
break;
|
|
596
|
+
case 'run-ended':
|
|
597
|
+
// Final cleanup — if status wasn't set to done/error/stopped yet
|
|
598
|
+
if (runs[runId] && runs[runId].status === 'running') {
|
|
599
|
+
setRunStatus(runId, 'stopped');
|
|
600
|
+
}
|
|
601
|
+
if (data.activeRuns === 0 && eventSource) {
|
|
602
|
+
eventSource.close(); eventSource = null; sseConnected = false;
|
|
603
|
+
}
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function handleStep(runId, data) {
|
|
609
|
+
const run = runs[runId];
|
|
610
|
+
if (!run) return;
|
|
611
|
+
const num = data.stepNum || getStepNum(data.name);
|
|
612
|
+
if (!num) return;
|
|
613
|
+
const el = run.el.querySelector('[data-step="' + num + '"]');
|
|
614
|
+
if (!el) return;
|
|
615
|
+
const statusEl = el.querySelector('.status');
|
|
616
|
+
|
|
617
|
+
el.className = 'p-step';
|
|
618
|
+
if (data.status === 'start') {
|
|
619
|
+
el.classList.add('running');
|
|
620
|
+
statusEl.textContent = 'Running...';
|
|
621
|
+
} else if (data.status === 'done') {
|
|
622
|
+
el.classList.add('done');
|
|
623
|
+
statusEl.textContent = data.name || 'Done';
|
|
624
|
+
} else if (data.status === 'skip') {
|
|
625
|
+
el.classList.add('skipped');
|
|
626
|
+
statusEl.textContent = 'Skipped';
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function handleResult(runId, data) {
|
|
631
|
+
const run = runs[runId];
|
|
632
|
+
if (!run) return;
|
|
633
|
+
run.resultCount++;
|
|
634
|
+
|
|
635
|
+
const tabId = runId + '-' + data.step.toLowerCase().replace(/\s+/g, '');
|
|
636
|
+
|
|
637
|
+
// Add to overview
|
|
638
|
+
const overview = run.el.querySelector('[data-pane="overview"]');
|
|
639
|
+
const placeholder = overview.querySelector('.no-results');
|
|
640
|
+
if (placeholder) placeholder.remove();
|
|
641
|
+
const summaryDiv = document.createElement('div');
|
|
642
|
+
summaryDiv.className = 'result-summary';
|
|
643
|
+
summaryDiv.innerHTML = '<strong>' + esc(data.step) + '</strong> — ' +
|
|
644
|
+
data.lines.toLocaleString() + ' lines → ' + esc(data.file.split(/[/\\]/).pop());
|
|
645
|
+
overview.appendChild(summaryDiv);
|
|
646
|
+
|
|
647
|
+
// Create result tab button
|
|
648
|
+
const tabs = run.el.querySelector('.result-tabs');
|
|
649
|
+
const btn = document.createElement('button');
|
|
650
|
+
btn.className = 'result-tab';
|
|
651
|
+
btn.setAttribute('data-tab', tabId);
|
|
652
|
+
btn.addEventListener('click', () => switchResultTab(runId, tabId));
|
|
653
|
+
btn.innerHTML = esc(data.step) + '<span class="tab-count">' + data.lines.toLocaleString() + '</span>';
|
|
654
|
+
tabs.appendChild(btn);
|
|
655
|
+
|
|
656
|
+
// Create result pane
|
|
657
|
+
const content = run.el.querySelector('.result-content');
|
|
658
|
+
const pane = document.createElement('div');
|
|
659
|
+
pane.className = 'tab-pane';
|
|
660
|
+
pane.setAttribute('data-pane', tabId);
|
|
661
|
+
pane.innerHTML = '<div class="result-summary">Loading...</div>';
|
|
662
|
+
content.appendChild(pane);
|
|
663
|
+
|
|
664
|
+
loadResultFile(data.file, runId, tabId);
|
|
665
|
+
run.el.querySelector('.results-count').textContent = run.resultCount + ' tools';
|
|
666
|
+
switchResultTab(runId, tabId);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function handleComplete(runId, data) {
|
|
670
|
+
const run = runs[runId];
|
|
671
|
+
if (!run) return;
|
|
672
|
+
const runDir = data.runDir || '';
|
|
673
|
+
if (runDir) {
|
|
674
|
+
const bar = run.el.querySelector('.run-dir-bar');
|
|
675
|
+
bar.style.display = 'block';
|
|
676
|
+
bar.innerHTML = '<strong>Output:</strong> ' + esc(runDir);
|
|
677
|
+
}
|
|
678
|
+
addRunLog(runId, 'success', 'Pipeline complete!');
|
|
679
|
+
setRunStatus(runId, 'done');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ---- Per-run logging ----
|
|
683
|
+
|
|
684
|
+
function addRunLog(runId, level, message) {
|
|
685
|
+
const run = runs[runId];
|
|
686
|
+
if (!run) return;
|
|
687
|
+
const panel = run.el.querySelector('.log-panel');
|
|
688
|
+
const line = document.createElement('div');
|
|
689
|
+
line.className = 'log-line log-' + level;
|
|
690
|
+
line.textContent = message;
|
|
691
|
+
panel.appendChild(line);
|
|
692
|
+
panel.scrollTop = panel.scrollHeight;
|
|
693
|
+
run.logLines++;
|
|
694
|
+
run.el.querySelector('.log-count').textContent = run.logLines + ' lines';
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ---- Result tabs within a run ----
|
|
698
|
+
|
|
699
|
+
function switchResultTab(runId, tabId) {
|
|
700
|
+
const run = runs[runId];
|
|
701
|
+
if (!run) return;
|
|
702
|
+
run.el.querySelectorAll('.result-tab').forEach(t => t.classList.remove('active'));
|
|
703
|
+
run.el.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
|
704
|
+
const btn = run.el.querySelector('[data-tab="' + tabId + '"]');
|
|
705
|
+
if (btn) btn.classList.add('active');
|
|
706
|
+
// Overview uses 'overview' as data-tab but also as data-pane
|
|
707
|
+
const pane = run.el.querySelector('[data-pane="' + tabId + '"]');
|
|
708
|
+
if (pane) pane.classList.add('active');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function loadResultFile(filePath, runId, tabId) {
|
|
712
|
+
const run = runs[runId];
|
|
713
|
+
if (!run) return;
|
|
714
|
+
const pane = run.el.querySelector('[data-pane="' + tabId + '"]');
|
|
715
|
+
if (!pane) return;
|
|
716
|
+
try {
|
|
717
|
+
const resp = await fetch('/file?path=' + encodeURIComponent(filePath));
|
|
718
|
+
if (!resp.ok) {
|
|
719
|
+
pane.innerHTML = '<div class="result-summary">Could not load file</div>';
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const text = await resp.text();
|
|
723
|
+
tabFileData[tabId] = { text, filePath };
|
|
724
|
+
const lines = text.trim().split('\n');
|
|
725
|
+
const max = 500;
|
|
726
|
+
let html = '<div class="result-toolbar">' +
|
|
727
|
+
'<span><strong>' + lines.length + ' lines</strong>' +
|
|
728
|
+
(lines.length > max ? ' (showing first ' + max + ')' : '') + '</span>' +
|
|
729
|
+
'<span class="toolbar-btns">' +
|
|
730
|
+
'<button class="tbtn" onclick="copyTab(\'' + tabId + '\')" title="Copy all">Copy All</button>' +
|
|
731
|
+
'<button class="tbtn" onclick="downloadTab(\'' + tabId + '\')" title="Download">Download</button>' +
|
|
732
|
+
'</span></div>';
|
|
733
|
+
const show = lines.slice(0, max);
|
|
734
|
+
for (const line of show) {
|
|
735
|
+
html += '<div class="result-line">' + esc(line) + '</div>';
|
|
736
|
+
}
|
|
737
|
+
pane.innerHTML = html;
|
|
738
|
+
} catch {
|
|
739
|
+
pane.innerHTML = '<div class="result-summary">Error loading file</div>';
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function copyTab(tabId) {
|
|
744
|
+
const data = tabFileData[tabId];
|
|
745
|
+
if (!data) return;
|
|
746
|
+
try {
|
|
747
|
+
await navigator.clipboard.writeText(data.text);
|
|
748
|
+
} catch {
|
|
749
|
+
const ta = document.createElement('textarea');
|
|
750
|
+
ta.value = data.text;
|
|
751
|
+
document.body.appendChild(ta);
|
|
752
|
+
ta.select();
|
|
753
|
+
document.execCommand('copy');
|
|
754
|
+
document.body.removeChild(ta);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function downloadTab(tabId) {
|
|
759
|
+
const data = tabFileData[tabId];
|
|
760
|
+
if (!data) return;
|
|
761
|
+
const name = data.filePath.split(/[/\\]/).pop() || 'results.txt';
|
|
762
|
+
const blob = new Blob([data.text], { type: 'text/plain' });
|
|
763
|
+
const url = URL.createObjectURL(blob);
|
|
764
|
+
const a = document.createElement('a');
|
|
765
|
+
a.href = url; a.download = name;
|
|
766
|
+
document.body.appendChild(a); a.click();
|
|
767
|
+
document.body.removeChild(a);
|
|
768
|
+
URL.revokeObjectURL(url);
|
|
769
|
+
}
|
|
770
|
+
</script>
|
|
771
|
+
</body>
|
|
772
|
+
</html>
|