@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.2",
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": ["recon", "bugbounty", "subdomain", "crawl", "pipeline"],
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-output",
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.html_output is not None
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-output requires --output to be set.")
900
+ parser.error("--html requires --output to be set.")
901
901
  base, _ = os.path.splitext(args.output)
902
- html_output_path = args.html_output if args.html_output else (base or args.output) + ".html"
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)} &nbsp;•&nbsp; 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 '&amp;';
474
+ case '<': return '&lt;';
475
+ case '>': return '&gt;';
476
+ case '"': return '&quot;';
477
+ case "'": return '&#39;';
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;
@@ -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 });