@paa1997/metho 1.0.2 → 1.0.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paa1997/metho",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Automated recon pipeline: subfinder → gau → filter → katana → findsomething",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
"start": "node bin/metho.js",
|
|
16
16
|
"gui": "node bin/metho.js --gui"
|
|
17
17
|
},
|
|
18
|
-
"keywords": [
|
|
18
|
+
"keywords": [
|
|
19
|
+
"recon",
|
|
20
|
+
"bugbounty",
|
|
21
|
+
"subdomain",
|
|
22
|
+
"crawl",
|
|
23
|
+
"pipeline"
|
|
24
|
+
],
|
|
19
25
|
"license": "MIT",
|
|
20
26
|
"dependencies": {
|
|
21
27
|
"chalk": "^5.4.1",
|
|
@@ -859,7 +859,7 @@ def main():
|
|
|
859
859
|
help="Output file path. Default: <list_file>_secrets.txt when --list is used.",
|
|
860
860
|
)
|
|
861
861
|
parser.add_argument(
|
|
862
|
-
"--html
|
|
862
|
+
"--html",
|
|
863
863
|
nargs="?",
|
|
864
864
|
const="",
|
|
865
865
|
help="Generate HTML report. Optional path (defaults to <output>.html).",
|
|
@@ -893,13 +893,13 @@ def main():
|
|
|
893
893
|
list_stem = os.path.splitext(os.path.basename(args.list.rstrip(os.sep)))[0] or "scan"
|
|
894
894
|
args.output = f"{list_stem}_secrets.txt"
|
|
895
895
|
|
|
896
|
-
html_requested = args.
|
|
896
|
+
html_requested = args.html is not None
|
|
897
897
|
html_output_path: Optional[str] = None
|
|
898
898
|
if html_requested:
|
|
899
899
|
if not args.output:
|
|
900
|
-
parser.error("--html
|
|
900
|
+
parser.error("--html requires --output to be set.")
|
|
901
901
|
base, _ = os.path.splitext(args.output)
|
|
902
|
-
html_output_path = args.
|
|
902
|
+
html_output_path = args.html if args.html else (base or args.output) + ".html"
|
|
903
903
|
|
|
904
904
|
# Reset globals per run
|
|
905
905
|
STOP_EVENT.clear()
|
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import re
|
|
3
|
+
import argparse
|
|
4
|
+
import html
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
HEADER_RE = re.compile(r"^=== Results for (.+?) ===\s*$")
|
|
10
|
+
SECRET_LINE_RE = re.compile(r"^\[secret\]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_results_streaming(filepath):
|
|
14
|
+
"""
|
|
15
|
+
Generator that yields blocks one at a time by reading the file line-by-line.
|
|
16
|
+
Each block is: {"url": "...", "secret_lines": [...], "has_secrets": bool}
|
|
17
|
+
"""
|
|
18
|
+
current_url = None
|
|
19
|
+
current_lines = []
|
|
20
|
+
|
|
21
|
+
def flush():
|
|
22
|
+
nonlocal current_url, current_lines
|
|
23
|
+
if current_url is None:
|
|
24
|
+
return None
|
|
25
|
+
secret_lines = [l.strip() for l in current_lines if SECRET_LINE_RE.match(l.strip())]
|
|
26
|
+
block = {
|
|
27
|
+
"url": current_url.strip(),
|
|
28
|
+
"secret_lines": secret_lines,
|
|
29
|
+
"has_secrets": bool(secret_lines),
|
|
30
|
+
}
|
|
31
|
+
current_url = None
|
|
32
|
+
current_lines = []
|
|
33
|
+
return block
|
|
34
|
+
|
|
35
|
+
with open(filepath, "r", encoding="utf-8", errors="replace") as fh:
|
|
36
|
+
for line in fh:
|
|
37
|
+
line = line.rstrip("\n\r")
|
|
38
|
+
m = HEADER_RE.match(line)
|
|
39
|
+
if m:
|
|
40
|
+
block = flush()
|
|
41
|
+
if block is not None:
|
|
42
|
+
yield block
|
|
43
|
+
current_url = m.group(1)
|
|
44
|
+
current_lines = []
|
|
45
|
+
else:
|
|
46
|
+
if current_url is not None:
|
|
47
|
+
current_lines.append(line)
|
|
48
|
+
|
|
49
|
+
block = flush()
|
|
50
|
+
if block is not None:
|
|
51
|
+
yield block
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_html_streaming(blocks_iter, out_fh, input_name="input"):
|
|
55
|
+
"""
|
|
56
|
+
Two-pass approach for large files:
|
|
57
|
+
Pass 1: iterate blocks, collect them in a list (only metadata, not raw text).
|
|
58
|
+
Pass 2: stream JSON array into the HTML file piece by piece.
|
|
59
|
+
|
|
60
|
+
This avoids holding the full JSON string in memory — we write each
|
|
61
|
+
element individually instead of json.dumps(entire_list).
|
|
62
|
+
"""
|
|
63
|
+
# Collect parsed blocks — this is the essential data we need and is much
|
|
64
|
+
# smaller than the raw input file (we already discarded non-secret lines).
|
|
65
|
+
blocks = list(blocks_iter)
|
|
66
|
+
|
|
67
|
+
total_blocks = len(blocks)
|
|
68
|
+
blocks_with_secrets = sum(1 for b in blocks if b["has_secrets"])
|
|
69
|
+
total_secret_lines = sum(len(b["secret_lines"]) for b in blocks if b["has_secrets"])
|
|
70
|
+
|
|
71
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
72
|
+
|
|
73
|
+
# We'll write the HTML in parts, streaming the JSON array element-by-element
|
|
74
|
+
# so we never build the full JSON string in memory.
|
|
75
|
+
|
|
76
|
+
w = out_fh.write
|
|
77
|
+
|
|
78
|
+
# --- Part 1: HTML head + CSS + body header (with dynamic stats) ---
|
|
79
|
+
w(f"""<!doctype html>
|
|
80
|
+
<html lang="en">
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="utf-8" />
|
|
83
|
+
<title>Secrets Report</title>
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
85
|
+
<style>
|
|
86
|
+
:root {{
|
|
87
|
+
--bg: #0f172a;
|
|
88
|
+
--panel: #111827;
|
|
89
|
+
--muted: #9ca3af;
|
|
90
|
+
--text: #e5e7eb;
|
|
91
|
+
--accent: #60a5fa;
|
|
92
|
+
--accent-2: #34d399;
|
|
93
|
+
--danger: #f87171;
|
|
94
|
+
--border: rgba(255,255,255,0.08);
|
|
95
|
+
--shadow: 0 10px 30px rgba(0,0,0,0.35);
|
|
96
|
+
--radius: 14px;
|
|
97
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
98
|
+
--sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans";
|
|
99
|
+
}}
|
|
100
|
+
body {{
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-family: var(--sans);
|
|
103
|
+
background:
|
|
104
|
+
radial-gradient(1200px 600px at 10% 10%, rgba(96,165,250,0.12), transparent),
|
|
105
|
+
radial-gradient(1200px 600px at 90% 20%, rgba(52,211,153,0.10), transparent),
|
|
106
|
+
var(--bg);
|
|
107
|
+
color: var(--text);
|
|
108
|
+
}}
|
|
109
|
+
.container {{
|
|
110
|
+
max-width: 1100px;
|
|
111
|
+
margin: 0 auto;
|
|
112
|
+
padding: 28px 18px 60px;
|
|
113
|
+
}}
|
|
114
|
+
header {{
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: 10px;
|
|
118
|
+
margin-bottom: 18px;
|
|
119
|
+
}}
|
|
120
|
+
h1 {{
|
|
121
|
+
font-size: clamp(22px, 3vw, 30px);
|
|
122
|
+
margin: 0;
|
|
123
|
+
}}
|
|
124
|
+
.meta {{
|
|
125
|
+
color: var(--muted);
|
|
126
|
+
font-size: 12px;
|
|
127
|
+
}}
|
|
128
|
+
|
|
129
|
+
.summary {{
|
|
130
|
+
display: grid;
|
|
131
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
132
|
+
gap: 10px;
|
|
133
|
+
margin: 16px 0 22px;
|
|
134
|
+
}}
|
|
135
|
+
.summary .box {{
|
|
136
|
+
background:
|
|
137
|
+
linear-gradient(180deg, rgba(255,255,255,0.02), transparent),
|
|
138
|
+
var(--panel);
|
|
139
|
+
border: 1px solid var(--border);
|
|
140
|
+
border-radius: var(--radius);
|
|
141
|
+
padding: 14px 14px 12px;
|
|
142
|
+
box-shadow: var(--shadow);
|
|
143
|
+
}}
|
|
144
|
+
.label {{
|
|
145
|
+
color: var(--muted);
|
|
146
|
+
font-size: 11px;
|
|
147
|
+
text-transform: uppercase;
|
|
148
|
+
letter-spacing: 0.08em;
|
|
149
|
+
}}
|
|
150
|
+
.value {{
|
|
151
|
+
font-size: 22px;
|
|
152
|
+
font-weight: 650;
|
|
153
|
+
margin-top: 4px;
|
|
154
|
+
}}
|
|
155
|
+
|
|
156
|
+
.toolbar {{
|
|
157
|
+
display: flex;
|
|
158
|
+
gap: 10px;
|
|
159
|
+
align-items: center;
|
|
160
|
+
margin-bottom: 12px;
|
|
161
|
+
flex-wrap: wrap;
|
|
162
|
+
}}
|
|
163
|
+
input[type="search"] {{
|
|
164
|
+
flex: 1;
|
|
165
|
+
min-width: 220px;
|
|
166
|
+
background: var(--panel);
|
|
167
|
+
border: 1px solid var(--border);
|
|
168
|
+
color: var(--text);
|
|
169
|
+
padding: 10px 12px;
|
|
170
|
+
border-radius: 10px;
|
|
171
|
+
outline: none;
|
|
172
|
+
}}
|
|
173
|
+
.toggle {{
|
|
174
|
+
display: inline-flex;
|
|
175
|
+
align-items: center;
|
|
176
|
+
gap: 8px;
|
|
177
|
+
background: var(--panel);
|
|
178
|
+
border: 1px solid var(--border);
|
|
179
|
+
padding: 8px 10px;
|
|
180
|
+
border-radius: 10px;
|
|
181
|
+
font-size: 12px;
|
|
182
|
+
color: var(--muted);
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
}}
|
|
185
|
+
.toggle input {{
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
transform: translateY(1px);
|
|
188
|
+
}}
|
|
189
|
+
|
|
190
|
+
button {{
|
|
191
|
+
background: rgba(96,165,250,0.12);
|
|
192
|
+
border: 1px solid rgba(96,165,250,0.35);
|
|
193
|
+
color: var(--text);
|
|
194
|
+
padding: 9px 12px;
|
|
195
|
+
border-radius: 10px;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
}}
|
|
200
|
+
button:hover {{
|
|
201
|
+
background: rgba(96,165,250,0.2);
|
|
202
|
+
}}
|
|
203
|
+
|
|
204
|
+
.status {{
|
|
205
|
+
display: flex;
|
|
206
|
+
gap: 10px;
|
|
207
|
+
align-items: center;
|
|
208
|
+
color: var(--muted);
|
|
209
|
+
font-size: 11px;
|
|
210
|
+
margin: 6px 0 14px;
|
|
211
|
+
}}
|
|
212
|
+
.dot {{
|
|
213
|
+
width: 6px;
|
|
214
|
+
height: 6px;
|
|
215
|
+
border-radius: 50%;
|
|
216
|
+
background: rgba(52,211,153,0.6);
|
|
217
|
+
display: inline-block;
|
|
218
|
+
}}
|
|
219
|
+
|
|
220
|
+
.cards {{
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
gap: 12px;
|
|
224
|
+
}}
|
|
225
|
+
|
|
226
|
+
details.card {{
|
|
227
|
+
background:
|
|
228
|
+
linear-gradient(180deg, rgba(255,255,255,0.02), transparent),
|
|
229
|
+
var(--panel);
|
|
230
|
+
border: 1px solid var(--border);
|
|
231
|
+
border-radius: var(--radius);
|
|
232
|
+
box-shadow: var(--shadow);
|
|
233
|
+
overflow: hidden;
|
|
234
|
+
}}
|
|
235
|
+
details.card summary {{
|
|
236
|
+
list-style: none;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
padding: 14px 14px;
|
|
239
|
+
display: grid;
|
|
240
|
+
grid-template-columns: auto 1fr auto;
|
|
241
|
+
gap: 10px;
|
|
242
|
+
align-items: center;
|
|
243
|
+
}}
|
|
244
|
+
details.card summary::-webkit-details-marker {{
|
|
245
|
+
display: none;
|
|
246
|
+
}}
|
|
247
|
+
|
|
248
|
+
.badge {{
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: center;
|
|
252
|
+
font-size: 10px;
|
|
253
|
+
padding: 3px 7px;
|
|
254
|
+
border-radius: 999px;
|
|
255
|
+
background: rgba(52,211,153,0.12);
|
|
256
|
+
border: 1px solid rgba(52,211,153,0.35);
|
|
257
|
+
color: #c7f9e8;
|
|
258
|
+
white-space: nowrap;
|
|
259
|
+
}}
|
|
260
|
+
|
|
261
|
+
a.url {{
|
|
262
|
+
font-size: 13px;
|
|
263
|
+
color: var(--accent);
|
|
264
|
+
text-decoration: none;
|
|
265
|
+
word-break: break-all;
|
|
266
|
+
}}
|
|
267
|
+
a.url:hover {{
|
|
268
|
+
text-decoration: underline;
|
|
269
|
+
}}
|
|
270
|
+
|
|
271
|
+
.right-pack {{
|
|
272
|
+
display: inline-flex;
|
|
273
|
+
align-items: center;
|
|
274
|
+
gap: 8px;
|
|
275
|
+
justify-self: end;
|
|
276
|
+
}}
|
|
277
|
+
|
|
278
|
+
.visited-wrap {{
|
|
279
|
+
display: inline-flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
gap: 6px;
|
|
282
|
+
font-size: 10px;
|
|
283
|
+
color: var(--muted);
|
|
284
|
+
background: rgba(255,255,255,0.03);
|
|
285
|
+
border: 1px solid var(--border);
|
|
286
|
+
padding: 4px 8px;
|
|
287
|
+
border-radius: 999px;
|
|
288
|
+
white-space: nowrap;
|
|
289
|
+
}}
|
|
290
|
+
.visited-wrap input {{
|
|
291
|
+
cursor: pointer;
|
|
292
|
+
transform: translateY(1px);
|
|
293
|
+
}}
|
|
294
|
+
|
|
295
|
+
.count {{
|
|
296
|
+
font-size: 11px;
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
white-space: nowrap;
|
|
299
|
+
}}
|
|
300
|
+
|
|
301
|
+
.card-body {{
|
|
302
|
+
border-top: 1px solid var(--border);
|
|
303
|
+
padding: 12px 16px 16px;
|
|
304
|
+
}}
|
|
305
|
+
|
|
306
|
+
ul.secrets {{
|
|
307
|
+
margin: 0;
|
|
308
|
+
padding-left: 18px;
|
|
309
|
+
display: flex;
|
|
310
|
+
flex-direction: column;
|
|
311
|
+
gap: 8px;
|
|
312
|
+
}}
|
|
313
|
+
.secret-item {{
|
|
314
|
+
display: flex;
|
|
315
|
+
gap: 8px;
|
|
316
|
+
align-items: center;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
}}
|
|
319
|
+
code {{
|
|
320
|
+
font-family: var(--mono);
|
|
321
|
+
font-size: 11.5px;
|
|
322
|
+
background: rgba(248,113,113,0.08);
|
|
323
|
+
border: 1px solid rgba(248,113,113,0.25);
|
|
324
|
+
padding: 2px 6px;
|
|
325
|
+
border-radius: 6px;
|
|
326
|
+
color: #ffd7d7;
|
|
327
|
+
word-break: break-all;
|
|
328
|
+
}}
|
|
329
|
+
.src-link {{
|
|
330
|
+
font-size: 10px;
|
|
331
|
+
color: #ffd7d7;
|
|
332
|
+
text-decoration: none;
|
|
333
|
+
border: 1px solid rgba(248,113,113,0.25);
|
|
334
|
+
padding: 2px 6px;
|
|
335
|
+
border-radius: 999px;
|
|
336
|
+
background: rgba(248,113,113,0.06);
|
|
337
|
+
}}
|
|
338
|
+
.src-link:hover {{
|
|
339
|
+
text-decoration: underline;
|
|
340
|
+
}}
|
|
341
|
+
|
|
342
|
+
.no-secrets-note {{
|
|
343
|
+
color: var(--muted);
|
|
344
|
+
font-size: 12px;
|
|
345
|
+
}}
|
|
346
|
+
|
|
347
|
+
.empty {{
|
|
348
|
+
background: var(--panel);
|
|
349
|
+
border: 1px dashed var(--border);
|
|
350
|
+
border-radius: var(--radius);
|
|
351
|
+
padding: 18px;
|
|
352
|
+
color: var(--muted);
|
|
353
|
+
}}
|
|
354
|
+
|
|
355
|
+
@media (max-width: 700px) {{
|
|
356
|
+
.summary {{ grid-template-columns: 1fr; }}
|
|
357
|
+
details.card summary {{ grid-template-columns: 1fr; }}
|
|
358
|
+
.right-pack {{ justify-self: start; }}
|
|
359
|
+
}}
|
|
360
|
+
</style>
|
|
361
|
+
</head>
|
|
362
|
+
<body>
|
|
363
|
+
<div class="container">
|
|
364
|
+
<header>
|
|
365
|
+
<h1>Secrets Report</h1>
|
|
366
|
+
<div class="meta">
|
|
367
|
+
Source: {html.escape(input_name)} • Generated: {now}
|
|
368
|
+
</div>
|
|
369
|
+
</header>
|
|
370
|
+
|
|
371
|
+
<section class="summary">
|
|
372
|
+
<div class="box">
|
|
373
|
+
<div class="label">Total result blocks found</div>
|
|
374
|
+
<div class="value">{total_blocks}</div>
|
|
375
|
+
</div>
|
|
376
|
+
<div class="box">
|
|
377
|
+
<div class="label">Blocks with secrets</div>
|
|
378
|
+
<div class="value">{blocks_with_secrets}</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="box">
|
|
381
|
+
<div class="label">Total secret lines</div>
|
|
382
|
+
<div class="value">{total_secret_lines}</div>
|
|
383
|
+
</div>
|
|
384
|
+
</section>
|
|
385
|
+
|
|
386
|
+
<div class="toolbar">
|
|
387
|
+
<input id="search" type="search" placeholder="Filter by URL or secret text..." />
|
|
388
|
+
|
|
389
|
+
<label class="toggle">
|
|
390
|
+
<input id="onlySecrets" type="checkbox" checked />
|
|
391
|
+
Show only secret entries
|
|
392
|
+
</label>
|
|
393
|
+
|
|
394
|
+
<label class="toggle">
|
|
395
|
+
<input id="hideVisited" type="checkbox" />
|
|
396
|
+
Hide visited
|
|
397
|
+
</label>
|
|
398
|
+
|
|
399
|
+
<button id="expandAll">Expand rendered</button>
|
|
400
|
+
<button id="collapseAll">Collapse rendered</button>
|
|
401
|
+
<button id="clearVisited">Clear visited</button>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<div class="status">
|
|
405
|
+
<span class="dot"></span>
|
|
406
|
+
<span id="statusText">Ready</span>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<main class="cards" id="cards">
|
|
410
|
+
<div class="empty" id="emptyBox" style="display:none;">No matching entries.</div>
|
|
411
|
+
</main>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<script>
|
|
415
|
+
const DATA = [""")
|
|
416
|
+
|
|
417
|
+
# --- Part 2: Stream JSON array elements one at a time ---
|
|
418
|
+
# We must escape '</' to '<\/' in JSON output, otherwise a secret line
|
|
419
|
+
# containing '</script>' will break the HTML parser out of the script tag.
|
|
420
|
+
for idx, block in enumerate(blocks):
|
|
421
|
+
if idx > 0:
|
|
422
|
+
w(",")
|
|
423
|
+
chunk = json.dumps(block, ensure_ascii=False)
|
|
424
|
+
w(chunk.replace("</", "<\\/"))
|
|
425
|
+
|
|
426
|
+
# --- Part 3: Close the JSON array and write the rest of the JS + HTML ---
|
|
427
|
+
w("""];
|
|
428
|
+
|
|
429
|
+
const cardsContainer = document.getElementById('cards');
|
|
430
|
+
const emptyBox = document.getElementById('emptyBox');
|
|
431
|
+
const searchEl = document.getElementById('search');
|
|
432
|
+
const onlySecretsEl = document.getElementById('onlySecrets');
|
|
433
|
+
const hideVisitedEl = document.getElementById('hideVisited');
|
|
434
|
+
const statusText = document.getElementById('statusText');
|
|
435
|
+
const expandAllBtn = document.getElementById('expandAll');
|
|
436
|
+
const collapseAllBtn = document.getElementById('collapseAll');
|
|
437
|
+
const clearVisitedBtn = document.getElementById('clearVisited');
|
|
438
|
+
|
|
439
|
+
let renderToken = 0;
|
|
440
|
+
|
|
441
|
+
// ---------------- Visited state ----------------
|
|
442
|
+
const VISITED_KEY = "secrets_report_visited_v1";
|
|
443
|
+
|
|
444
|
+
function loadVisited() {
|
|
445
|
+
try {
|
|
446
|
+
const raw = localStorage.getItem(VISITED_KEY);
|
|
447
|
+
const arr = raw ? JSON.parse(raw) : [];
|
|
448
|
+
return new Set(Array.isArray(arr) ? arr : []);
|
|
449
|
+
} catch {
|
|
450
|
+
return new Set();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function saveVisited(set) {
|
|
455
|
+
localStorage.setItem(VISITED_KEY, JSON.stringify(Array.from(set)));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
let visitedSet = loadVisited();
|
|
459
|
+
|
|
460
|
+
function applyVisitedUI(root = document) {
|
|
461
|
+
root.querySelectorAll('details.card').forEach(card => {
|
|
462
|
+
const url = card.dataset.url;
|
|
463
|
+
const box = card.querySelector('.visited-box');
|
|
464
|
+
if (!box) return;
|
|
465
|
+
box.checked = visitedSet.has(url);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ---------------- Helpers ----------------
|
|
470
|
+
function escapeHtml(str) {
|
|
471
|
+
return str.replace(/[&<>"']/g, s => {
|
|
472
|
+
switch (s) {
|
|
473
|
+
case '&': return '&';
|
|
474
|
+
case '<': return '<';
|
|
475
|
+
case '>': return '>';
|
|
476
|
+
case '"': return '"';
|
|
477
|
+
case "'": return ''';
|
|
478
|
+
default: return s;
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseSecretLine(line) {
|
|
484
|
+
const m = line.match(/^(.*?)(\\s*\\[(https?:\\/\\/[^\\]]+)\\]\\s*)$/);
|
|
485
|
+
if (!m) {
|
|
486
|
+
return `<li><code>${escapeHtml(line)}</code></li>`;
|
|
487
|
+
}
|
|
488
|
+
const before = m[1].trim();
|
|
489
|
+
const url = m[3].trim();
|
|
490
|
+
const beforeEsc = escapeHtml(before);
|
|
491
|
+
const urlEsc = escapeHtml(url);
|
|
492
|
+
return `
|
|
493
|
+
<li class="secret-item">
|
|
494
|
+
<code>${beforeEsc}</code>
|
|
495
|
+
<a class="src-link" href="${urlEsc}" target="_blank" rel="noopener noreferrer">source</a>
|
|
496
|
+
</li>
|
|
497
|
+
`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function buildCardHTML(item, index) {
|
|
501
|
+
const urlEsc = escapeHtml(item.url);
|
|
502
|
+
const badge = `#${index + 1}`;
|
|
503
|
+
const count = item.secret_lines.length;
|
|
504
|
+
|
|
505
|
+
let bodyHtml = '';
|
|
506
|
+
if (item.has_secrets) {
|
|
507
|
+
const secrets = item.secret_lines.map(parseSecretLine).join('');
|
|
508
|
+
bodyHtml = `<ul class="secrets">${secrets}</ul>`;
|
|
509
|
+
} else {
|
|
510
|
+
bodyHtml = `<div class="no-secrets-note">No secrets detected.</div>`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const openAttr = item.has_secrets ? 'open' : '';
|
|
514
|
+
|
|
515
|
+
return `
|
|
516
|
+
<details class="card" data-url="${urlEsc}" data-has-secrets="${item.has_secrets}" ${openAttr}>
|
|
517
|
+
<summary>
|
|
518
|
+
<span class="badge">${badge}</span>
|
|
519
|
+
<a class="url" href="${urlEsc}" target="_blank" rel="noopener noreferrer">${urlEsc}</a>
|
|
520
|
+
<span class="right-pack">
|
|
521
|
+
<label class="visited-wrap">
|
|
522
|
+
<input class="visited-box" type="checkbox" />
|
|
523
|
+
Visited
|
|
524
|
+
</label>
|
|
525
|
+
<span class="count">${count} secret(s)</span>
|
|
526
|
+
</span>
|
|
527
|
+
</summary>
|
|
528
|
+
<div class="card-body">
|
|
529
|
+
${bodyHtml}
|
|
530
|
+
</div>
|
|
531
|
+
</details>
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function getFilteredData() {
|
|
536
|
+
const q = searchEl.value.toLowerCase().trim();
|
|
537
|
+
const onlySecrets = onlySecretsEl.checked;
|
|
538
|
+
const hideVisited = hideVisitedEl.checked;
|
|
539
|
+
|
|
540
|
+
return DATA.filter(item => {
|
|
541
|
+
const urlLower = item.url.toLowerCase();
|
|
542
|
+
|
|
543
|
+
if (onlySecrets && !item.has_secrets) return false;
|
|
544
|
+
if (hideVisited && visitedSet.has(item.url)) return false;
|
|
545
|
+
|
|
546
|
+
if (!q) return true;
|
|
547
|
+
|
|
548
|
+
if (urlLower.includes(q)) return true;
|
|
549
|
+
|
|
550
|
+
if (item.has_secrets) {
|
|
551
|
+
return item.secret_lines.some(s => s.toLowerCase().includes(q));
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function clearCards() {
|
|
558
|
+
const nodes = Array.from(cardsContainer.querySelectorAll('details.card'));
|
|
559
|
+
nodes.forEach(n => n.remove());
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function setStatus(msg) {
|
|
563
|
+
statusText.textContent = msg;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
let scheduleRender = null;
|
|
567
|
+
|
|
568
|
+
function wireVisitedForRenderedCards() {
|
|
569
|
+
cardsContainer.querySelectorAll('details.card').forEach(card => {
|
|
570
|
+
if (card.dataset.visitedWired) return;
|
|
571
|
+
card.dataset.visitedWired = "1";
|
|
572
|
+
|
|
573
|
+
const url = card.dataset.url;
|
|
574
|
+
const box = card.querySelector('.visited-box');
|
|
575
|
+
const link = card.querySelector('a.url');
|
|
576
|
+
|
|
577
|
+
if (box) {
|
|
578
|
+
box.addEventListener('change', () => {
|
|
579
|
+
if (box.checked) visitedSet.add(url);
|
|
580
|
+
else visitedSet.delete(url);
|
|
581
|
+
saveVisited(visitedSet);
|
|
582
|
+
|
|
583
|
+
if (hideVisitedEl.checked && scheduleRender) {
|
|
584
|
+
scheduleRender();
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (link) {
|
|
590
|
+
link.addEventListener('click', () => {
|
|
591
|
+
visitedSet.add(url);
|
|
592
|
+
saveVisited(visitedSet);
|
|
593
|
+
if (box) box.checked = true;
|
|
594
|
+
|
|
595
|
+
if (hideVisitedEl.checked && scheduleRender) {
|
|
596
|
+
scheduleRender();
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function renderIncrementally(items) {
|
|
604
|
+
const myToken = ++renderToken;
|
|
605
|
+
clearCards();
|
|
606
|
+
|
|
607
|
+
if (items.length === 0) {
|
|
608
|
+
emptyBox.style.display = '';
|
|
609
|
+
setStatus('No matches');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
emptyBox.style.display = 'none';
|
|
613
|
+
|
|
614
|
+
const chunkSize = 200;
|
|
615
|
+
let i = 0;
|
|
616
|
+
|
|
617
|
+
setStatus(`Rendering 0 / ${items.length}`);
|
|
618
|
+
|
|
619
|
+
function step() {
|
|
620
|
+
if (myToken !== renderToken) return;
|
|
621
|
+
|
|
622
|
+
const end = Math.min(i + chunkSize, items.length);
|
|
623
|
+
let htmlChunk = '';
|
|
624
|
+
for (; i < end; i++) {
|
|
625
|
+
htmlChunk += buildCardHTML(items[i], i);
|
|
626
|
+
}
|
|
627
|
+
cardsContainer.insertAdjacentHTML('beforeend', htmlChunk);
|
|
628
|
+
|
|
629
|
+
applyVisitedUI(cardsContainer);
|
|
630
|
+
wireVisitedForRenderedCards();
|
|
631
|
+
|
|
632
|
+
setStatus(`Rendering ${end} / ${items.length}`);
|
|
633
|
+
|
|
634
|
+
if (i < items.length) {
|
|
635
|
+
requestAnimationFrame(step);
|
|
636
|
+
} else {
|
|
637
|
+
setStatus(`Done \u2022 Rendered ${items.length} entries`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
requestAnimationFrame(step);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ---------------- Debounce search/render ----------------
|
|
645
|
+
let searchTimer = null;
|
|
646
|
+
scheduleRender = function() {
|
|
647
|
+
if (searchTimer) clearTimeout(searchTimer);
|
|
648
|
+
searchTimer = setTimeout(() => {
|
|
649
|
+
renderIncrementally(getFilteredData());
|
|
650
|
+
}, 120);
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
searchEl.addEventListener('input', scheduleRender);
|
|
654
|
+
onlySecretsEl.addEventListener('change', scheduleRender);
|
|
655
|
+
hideVisitedEl.addEventListener('change', scheduleRender);
|
|
656
|
+
|
|
657
|
+
expandAllBtn.addEventListener('click', () => {
|
|
658
|
+
document.querySelectorAll('details.card').forEach(d => d.open = true);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
collapseAllBtn.addEventListener('click', () => {
|
|
662
|
+
document.querySelectorAll('details.card').forEach(d => d.open = false);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
clearVisitedBtn.addEventListener('click', () => {
|
|
666
|
+
visitedSet = new Set();
|
|
667
|
+
saveVisited(visitedSet);
|
|
668
|
+
applyVisitedUI(cardsContainer);
|
|
669
|
+
|
|
670
|
+
if (hideVisitedEl.checked) {
|
|
671
|
+
scheduleRender();
|
|
672
|
+
} else {
|
|
673
|
+
setStatus("Visited cleared");
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Initial render
|
|
678
|
+
renderIncrementally(getFilteredData());
|
|
679
|
+
</script>
|
|
680
|
+
</body>
|
|
681
|
+
</html>
|
|
682
|
+
""")
|
|
683
|
+
|
|
684
|
+
return total_blocks, blocks_with_secrets
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def main():
|
|
688
|
+
ap = argparse.ArgumentParser(
|
|
689
|
+
description="Re-organize secret scanner results into a neat HTML report (fast, visited tracking + hide visited filter)."
|
|
690
|
+
)
|
|
691
|
+
ap.add_argument("-i", "--input", required=True, help="Input text file with results.")
|
|
692
|
+
ap.add_argument("-o", "--output", required=True, help="Output HTML file.")
|
|
693
|
+
args = ap.parse_args()
|
|
694
|
+
|
|
695
|
+
in_path = Path(args.input)
|
|
696
|
+
out_path = Path(args.output)
|
|
697
|
+
|
|
698
|
+
blocks_iter = parse_results_streaming(in_path)
|
|
699
|
+
|
|
700
|
+
with open(out_path, "w", encoding="utf-8") as out_fh:
|
|
701
|
+
total_blocks, blocks_with_secrets = write_html_streaming(
|
|
702
|
+
blocks_iter, out_fh, input_name=in_path.name
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
print(f"[+] Parsed blocks: {total_blocks}")
|
|
706
|
+
print(f"[+] Blocks with secrets: {blocks_with_secrets}")
|
|
707
|
+
print(f"[+] Wrote HTML: {out_path}")
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
if __name__ == "__main__":
|
|
711
|
+
main()
|
|
@@ -34,7 +34,7 @@ export class FindSomethingStep extends BaseStep {
|
|
|
34
34
|
|
|
35
35
|
this.logger.debug(`Using findsomething script: ${scriptPath}`);
|
|
36
36
|
const pythonBin = isWin ? 'python' : 'python3';
|
|
37
|
-
const args = [scriptPath, '-l', inputFile, '-o', outputFile];
|
|
37
|
+
const args = [scriptPath, '-l', inputFile, '-o', outputFile, '--html'];
|
|
38
38
|
|
|
39
39
|
const result = await this.runCommand(pythonBin, args, { outputFile });
|
|
40
40
|
return result;
|
package/src/steps/katana.js
CHANGED
|
@@ -14,10 +14,13 @@ export class KatanaStep extends BaseStep {
|
|
|
14
14
|
const chunkSize = config.katanaChunkSize;
|
|
15
15
|
const depth = config.katanaDepth;
|
|
16
16
|
|
|
17
|
+
// Exclude media, images, fonts, stylesheets and other non-useful extensions
|
|
18
|
+
const excludeExt = 'png,jpg,jpeg,gif,svg,ico,webp,bmp,tiff,mp4,mp3,avi,mov,wmv,flv,webm,ogg,wav,css,woff,woff2,ttf,eot,otf,pdf,zip,gz,tar,rar,7z,mp2,mkv';
|
|
19
|
+
|
|
17
20
|
if (lineCount <= chunkSize) {
|
|
18
21
|
// Small enough to run in one shot
|
|
19
22
|
this.logger.debug(`Katana: ${lineCount} URLs, running single batch`);
|
|
20
|
-
const args = ['-list', inputFile, '-d', String(depth), '-o', outputFile];
|
|
23
|
+
const args = ['-list', inputFile, '-d', String(depth), '-ef', excludeExt, '-o', outputFile];
|
|
21
24
|
return this.runCommand('katana', args, { outputFile });
|
|
22
25
|
}
|
|
23
26
|
|
|
@@ -32,7 +35,7 @@ export class KatanaStep extends BaseStep {
|
|
|
32
35
|
for (let i = 0; i < chunks.length; i++) {
|
|
33
36
|
this.logger.info(`Katana chunk ${i + 1}/${chunks.length}...`);
|
|
34
37
|
const chunkOutput = join(tmpDir, `katana-out-${i}.txt`);
|
|
35
|
-
const args = ['-list', chunks[i], '-d', String(depth), '-o', chunkOutput];
|
|
38
|
+
const args = ['-list', chunks[i], '-d', String(depth), '-ef', excludeExt, '-o', chunkOutput];
|
|
36
39
|
|
|
37
40
|
try {
|
|
38
41
|
await this.runCommand('katana', args, { outputFile: chunkOutput });
|