detecti-cli 2.0.0__py3-none-any.whl

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.
Files changed (64) hide show
  1. detecti/__init__.py +0 -0
  2. detecti/cli.py +649 -0
  3. detecti/config.py +188 -0
  4. detecti/core/__init__.py +1 -0
  5. detecti/core/database/__init__.py +5 -0
  6. detecti/core/database/config_db.py +73 -0
  7. detecti/core/database/schema.py +136 -0
  8. detecti/core/database/storage.py +1388 -0
  9. detecti/core/engine.py +1032 -0
  10. detecti/core/models.py +278 -0
  11. detecti/data/config.sqlite +0 -0
  12. detecti/data/dbs/.gitkeep +2 -0
  13. detecti/data/dbs/example.com.sqlite +0 -0
  14. detecti/modules/__init__.py +29 -0
  15. detecti/modules/base.py +57 -0
  16. detecti/modules/censys.py +813 -0
  17. detecti/modules/crtsh.py +98 -0
  18. detecti/modules/exploitdb.py +138 -0
  19. detecti/modules/masscan.py +561 -0
  20. detecti/modules/nuclei.py +449 -0
  21. detecti/modules/nvd.py +300 -0
  22. detecti/modules/reverse_whois.py +225 -0
  23. detecti/modules/shodan.py +412 -0
  24. detecti/reporters/__init__.py +7 -0
  25. detecti/reporters/csv_reporter.py +74 -0
  26. detecti/reporters/html_reporter.py +356 -0
  27. detecti/reporters/json_reporter.py +26 -0
  28. detecti/reporters/markdown_reporter.py +203 -0
  29. detecti/utils/__init__.py +1 -0
  30. detecti/utils/http.py +294 -0
  31. detecti/utils/logger.py +378 -0
  32. detecti/utils/setup.py +453 -0
  33. detecti/web/__init__.py +6 -0
  34. detecti/web/api/__init__.py +1 -0
  35. detecti/web/api/auth.py +109 -0
  36. detecti/web/api/graph_builder.py +901 -0
  37. detecti/web/api/routes.py +1602 -0
  38. detecti/web/process_manager.py +283 -0
  39. detecti/web/server.py +183 -0
  40. detecti/web/static/android-chrome-192x192.png +0 -0
  41. detecti/web/static/android-chrome-512x512.png +0 -0
  42. detecti/web/static/apple-touch-icon.png +0 -0
  43. detecti/web/static/css/__init__.py +1 -0
  44. detecti/web/static/css/dashboard.css +3802 -0
  45. detecti/web/static/favicon-16x16.png +0 -0
  46. detecti/web/static/favicon-32x32.png +0 -0
  47. detecti/web/static/favicon.ico +0 -0
  48. detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
  49. detecti/web/static/img/detecti-ico.png +0 -0
  50. detecti/web/static/index.html +677 -0
  51. detecti/web/static/js/__init__.py +1 -0
  52. detecti/web/static/js/api.js +177 -0
  53. detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
  54. detecti/web/static/js/cytoscape-dagre.js +397 -0
  55. detecti/web/static/js/cytoscape.min.js +31 -0
  56. detecti/web/static/js/dagre.min.js +3809 -0
  57. detecti/web/static/js/graph.js +7439 -0
  58. detecti/web/static/js/lucide.min.js +12 -0
  59. detecti/web/static/login.html +290 -0
  60. detecti/web/static/site.webmanifest +1 -0
  61. detecti_cli-2.0.0.dist-info/METADATA +554 -0
  62. detecti_cli-2.0.0.dist-info/RECORD +64 -0
  63. detecti_cli-2.0.0.dist-info/WHEEL +4 -0
  64. detecti_cli-2.0.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,356 @@
1
+ """HTML Reporter for DetecTI Scans.
2
+
3
+ Converts the executive Markdown report into a modern, standalone HTML document
4
+ ready for direct browser interpretation, styling, and printing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ try:
14
+ import markdown
15
+ MARKDOWN_LIB_AVAILABLE = True
16
+ except ImportError:
17
+ MARKDOWN_LIB_AVAILABLE = False
18
+
19
+ from detecti.core.models import ScanResult
20
+ from detecti.reporters.markdown_reporter import MarkdownReporter
21
+
22
+
23
+ class HTMLReporter:
24
+ """Generates an executive, beautifully styled standalone HTML report from ScanResult."""
25
+
26
+ HTML_TEMPLATE = """<!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>DetecTI Security Intelligence Report - {target}</title>
32
+ <style>
33
+ :root {{
34
+ --bg-color: #0d1117;
35
+ --card-bg: #161b22;
36
+ --card-border: #30363d;
37
+ --text-color: #c9d1d9;
38
+ --text-heading: #f0f6fc;
39
+ --accent-cyan: #58a6ff;
40
+ --accent-blue: #1f6feb;
41
+ --risk-critical: #f85149;
42
+ --risk-high: #ff7b72;
43
+ --risk-medium: #d29922;
44
+ --risk-low: #3fb950;
45
+ --table-row-alt: #1c2128;
46
+ --code-bg: #21262d;
47
+ }}
48
+
49
+ * {{
50
+ box-sizing: border-box;
51
+ margin: 0;
52
+ padding: 0;
53
+ }}
54
+
55
+ body {{
56
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
57
+ background-color: var(--bg-color);
58
+ color: var(--text-color);
59
+ line-height: 1.6;
60
+ padding: 2rem 1rem;
61
+ }}
62
+
63
+ .container {{
64
+ max-width: 1100px;
65
+ margin: 0 auto;
66
+ background: var(--card-bg);
67
+ border: 1px solid var(--card-border);
68
+ border-radius: 8px;
69
+ padding: 2.5rem;
70
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
71
+ }}
72
+
73
+ .report-header {{
74
+ border-bottom: 2px solid #30363d;
75
+ padding-bottom: 1.5rem;
76
+ margin-bottom: 2rem;
77
+ display: flex;
78
+ justify-content: space-between;
79
+ align-items: center;
80
+ flex-wrap: wrap;
81
+ gap: 1rem;
82
+ }}
83
+
84
+ .header-title-group h1 {{
85
+ color: var(--accent-cyan);
86
+ font-size: 1.8rem;
87
+ font-weight: 700;
88
+ margin-bottom: 0.25rem;
89
+ }}
90
+
91
+ .header-title-group .subtitle {{
92
+ color: #8b949e;
93
+ font-size: 0.9rem;
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.5px;
96
+ }}
97
+
98
+ .header-actions {{
99
+ display: flex;
100
+ gap: 0.5rem;
101
+ }}
102
+
103
+ .btn-print {{
104
+ background: var(--code-bg);
105
+ border: 1px solid var(--card-border);
106
+ color: var(--text-heading);
107
+ padding: 0.45rem 0.9rem;
108
+ border-radius: 6px;
109
+ font-size: 0.85rem;
110
+ font-weight: 600;
111
+ cursor: pointer;
112
+ transition: all 0.2s;
113
+ }}
114
+
115
+ .btn-print:hover {{
116
+ background: var(--accent-blue);
117
+ border-color: var(--accent-cyan);
118
+ color: #ffffff;
119
+ }}
120
+
121
+ h1, h2, h3, h4, h5, h6 {{
122
+ color: var(--text-heading);
123
+ margin-top: 1.75rem;
124
+ margin-bottom: 0.85rem;
125
+ font-weight: 600;
126
+ }}
127
+
128
+ h1 {{ font-size: 1.75rem; color: var(--accent-cyan); border-bottom: 1px solid var(--card-border); padding-bottom: 0.4rem; }}
129
+ h2 {{ font-size: 1.35rem; color: #79c0ff; border-bottom: 1px solid #21262d; padding-bottom: 0.3rem; margin-top: 2rem; }}
130
+ h3 {{ font-size: 1.15rem; color: #a5d6ff; }}
131
+ h4 {{ font-size: 1.05rem; color: #d2a8ff; }}
132
+ h5 {{ font-size: 0.95rem; color: #ffa657; }}
133
+
134
+ p {{
135
+ margin-bottom: 1rem;
136
+ }}
137
+
138
+ blockquote {{
139
+ border-left: 4px solid var(--accent-cyan);
140
+ padding: 0.6rem 1rem;
141
+ background: rgba(88, 166, 255, 0.08);
142
+ margin: 1rem 0 1.5rem 0;
143
+ border-radius: 0 6px 6px 0;
144
+ color: #8b949e;
145
+ font-size: 0.92rem;
146
+ }}
147
+
148
+ blockquote p {{
149
+ margin-bottom: 0.3rem;
150
+ }}
151
+
152
+ blockquote p:last-child {{
153
+ margin-bottom: 0;
154
+ }}
155
+
156
+ /* Table Styling */
157
+ .table-wrapper {{
158
+ overflow-x: auto;
159
+ margin: 1rem 0 1.75rem 0;
160
+ border: 1px solid var(--card-border);
161
+ border-radius: 6px;
162
+ }}
163
+
164
+ table {{
165
+ width: 100%;
166
+ border-collapse: collapse;
167
+ font-size: 0.88rem;
168
+ text-align: left;
169
+ }}
170
+
171
+ th {{
172
+ background-color: #1f242c;
173
+ color: #f0f6fc;
174
+ padding: 0.75rem 0.9rem;
175
+ font-weight: 600;
176
+ border-bottom: 1px solid var(--card-border);
177
+ white-space: nowrap;
178
+ }}
179
+
180
+ td {{
181
+ padding: 0.65rem 0.9rem;
182
+ border-bottom: 1px solid #21262d;
183
+ vertical-align: middle;
184
+ }}
185
+
186
+ tr:nth-child(even) {{
187
+ background-color: rgba(255, 255, 255, 0.02);
188
+ }}
189
+
190
+ tr:hover td {{
191
+ background-color: rgba(88, 166, 255, 0.06);
192
+ }}
193
+
194
+ /* Code & Badges */
195
+ code {{
196
+ background-color: var(--code-bg);
197
+ color: #79c0ff;
198
+ padding: 0.2rem 0.4rem;
199
+ border-radius: 4px;
200
+ font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
201
+ font-size: 0.85em;
202
+ border: 1px solid rgba(110, 118, 129, 0.4);
203
+ }}
204
+
205
+ pre {{
206
+ background-color: var(--code-bg);
207
+ border: 1px solid var(--card-border);
208
+ border-radius: 6px;
209
+ padding: 1rem;
210
+ overflow-x: auto;
211
+ margin: 1rem 0;
212
+ }}
213
+
214
+ pre code {{
215
+ background: none;
216
+ border: none;
217
+ padding: 0;
218
+ color: var(--text-color);
219
+ }}
220
+
221
+ a {{
222
+ color: var(--accent-cyan);
223
+ text-decoration: none;
224
+ }}
225
+
226
+ a:hover {{
227
+ text-decoration: underline;
228
+ }}
229
+
230
+ hr {{
231
+ border: 0;
232
+ height: 1px;
233
+ background: var(--card-border);
234
+ margin: 2rem 0;
235
+ }}
236
+
237
+ ul, ol {{
238
+ padding-left: 1.75rem;
239
+ margin-bottom: 1rem;
240
+ }}
241
+
242
+ li {{
243
+ margin-bottom: 0.35rem;
244
+ }}
245
+
246
+ .footer-note {{
247
+ margin-top: 3rem;
248
+ text-align: center;
249
+ font-size: 0.8rem;
250
+ color: #8b949e;
251
+ border-top: 1px solid var(--card-border);
252
+ padding-top: 1.5rem;
253
+ }}
254
+
255
+ @media print {{
256
+ body {{
257
+ background: #ffffff;
258
+ color: #000000;
259
+ padding: 0;
260
+ }}
261
+ .container {{
262
+ box-shadow: none;
263
+ border: none;
264
+ padding: 0;
265
+ max-width: 100%;
266
+ }}
267
+ .btn-print {{
268
+ display: none;
269
+ }}
270
+ th {{
271
+ background: #f0f0f0;
272
+ color: #000000;
273
+ }}
274
+ code {{
275
+ background: #f5f5f5;
276
+ color: #000000;
277
+ border-color: #ccc;
278
+ }}
279
+ blockquote {{
280
+ background: #f9f9f9;
281
+ border-left-color: #007bff;
282
+ }}
283
+ h1, h2, h3, h4 {{
284
+ color: #000000;
285
+ }}
286
+ }}
287
+ </style>
288
+ </head>
289
+ <body>
290
+ <div class="container">
291
+ <div class="report-header">
292
+ <div class="header-title-group">
293
+ <h1>DetecTI-CLI Intelligence Report</h1>
294
+ <div class="subtitle">External Attack Surface Management &amp; Vulnerability Assessment &bull; <a href="https://detecti.com.br" target="_blank" rel="noopener noreferrer">detecti.com.br</a></div>
295
+ </div>
296
+ <div class="header-actions">
297
+ <button class="btn-print" onclick="window.print()">🖨️ Print / Save PDF</button>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="report-body">
302
+ {content}
303
+ </div>
304
+
305
+ <div class="footer-note">
306
+ Generated automatically by <strong>DetecTI-CLI v2.0</strong> — External Attack Surface Mapping &amp; Threat Intelligence Engine.<br>
307
+ Powered by <a href="https://detecti.com.br" target="_blank" rel="noopener noreferrer"><strong>DetecTI Security</strong> (detecti.com.br)</a>
308
+ </div>
309
+ </div>
310
+ </body>
311
+ </html>
312
+ """
313
+
314
+ @classmethod
315
+ def generate(cls, result: ScanResult) -> str:
316
+ """Generate standalone HTML document from ScanResult."""
317
+ md_text = MarkdownReporter.generate(result)
318
+
319
+ if MARKDOWN_LIB_AVAILABLE:
320
+ html_body = markdown.markdown(
321
+ md_text,
322
+ extensions=["tables", "fenced_code", "nl2br", "sane_lists"]
323
+ )
324
+ else:
325
+ # Simple fallback rendering if markdown package is missing
326
+ lines = []
327
+ for line in md_text.splitlines():
328
+ if line.startswith("# "):
329
+ lines.append(f"<h1>{html.escape(line[2:])}</h1>")
330
+ elif line.startswith("## "):
331
+ lines.append(f"<h2>{html.escape(line[3:])}</h2>")
332
+ elif line.startswith("### "):
333
+ lines.append(f"<h3>{html.escape(line[4:])}</h3>")
334
+ elif line.startswith("> "):
335
+ lines.append(f"<blockquote><p>{html.escape(line[2:])}</p></blockquote>")
336
+ elif line.strip() == "---":
337
+ lines.append("<hr>")
338
+ elif line.strip():
339
+ lines.append(f"<p>{html.escape(line)}</p>")
340
+ html_body = "\n".join(lines)
341
+
342
+ # Wrap tables in responsive wrapper div if not wrapped
343
+ if "<table>" in html_body and '<div class="table-wrapper">' not in html_body:
344
+ html_body = html_body.replace("<table>", '<div class="table-wrapper"><table>').replace("</table>", "</table></div>")
345
+
346
+ safe_target = html.escape(str(result.target))
347
+ return cls.HTML_TEMPLATE.format(target=safe_target, content=html_body)
348
+
349
+ @classmethod
350
+ def save(cls, result: ScanResult, output_path: Path | str) -> Path:
351
+ """Save formatted HTML report to disk."""
352
+ path = Path(output_path)
353
+ path.parent.mkdir(parents=True, exist_ok=True)
354
+ content = cls.generate(result)
355
+ path.write_text(content, encoding="utf-8")
356
+ return path
@@ -0,0 +1,26 @@
1
+ """JSON Reporter for structured export."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Optional
8
+ from detecti.core.models import ScanResult
9
+
10
+
11
+ class JSONReporter:
12
+ """Exports ScanResult to structured JSON."""
13
+
14
+ @staticmethod
15
+ def generate(result: ScanResult, indent: int = 2) -> str:
16
+ """Serialize ScanResult to formatted JSON string."""
17
+ return result.model_dump_json(indent=indent)
18
+
19
+ @classmethod
20
+ def save(cls, result: ScanResult, output_path: Path | str, indent: int = 2) -> Path:
21
+ """Save formatted JSON report to disk."""
22
+ path = Path(output_path)
23
+ path.parent.mkdir(parents=True, exist_ok=True)
24
+ content = cls.generate(result, indent=indent)
25
+ path.write_text(content, encoding="utf-8")
26
+ return path
@@ -0,0 +1,203 @@
1
+ """Executive Markdown Reporter for DetecTI Scans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Dict, List, Set
7
+ from detecti.core.models import FindingType, ScanResult, SeverityLevel
8
+
9
+
10
+ class MarkdownReporter:
11
+ """Generates an executive, structured Markdown security intelligence report."""
12
+
13
+ @classmethod
14
+ def generate(cls, result: ScanResult) -> str:
15
+ """Render complete Markdown document from ScanResult."""
16
+ lines: List[str] = []
17
+
18
+ # 1. Header
19
+ lines.append(f"# DetecTI Cyber Lead Intelligence Report: `{result.target}`")
20
+ lines.append("")
21
+ lines.append(f"> **Scan ID:** `{result.scan_id}` ")
22
+ lines.append(f"> **Target Type:** `{result.target_type}` ")
23
+ lines.append(f"> **Execution Date:** {result.started_at.strftime('%Y-%m-%d %H:%M:%S UTC')} ")
24
+ lines.append(f"> **Duration:** {result.elapsed_seconds:.2f} seconds ")
25
+ lines.append(f"> **Modules Active:** `{', '.join(result.modules_run)}` ")
26
+ lines.append("")
27
+
28
+ # 2. Executive Summary Metrics
29
+ summary = result.summary
30
+ lines.append("## 1. Executive Summary")
31
+ lines.append("")
32
+ lines.append("| Metric | Count | Details |")
33
+ lines.append("| :--- | :---: | :--- |")
34
+ lines.append(f"| **Total Findings** | `{summary.total_findings}` | Total assets, ports, and intelligence records |")
35
+ lines.append(f"| **Total Hosts Mapped** | `{summary.total_hosts_count}` | Individual IP addresses analyzed |")
36
+ lines.append(f"| **Subdomains Discovered** | `{summary.subdomains_count}` | Certificate Transparency & DNS enumeration |")
37
+ lines.append(f"| **Associated Domains** | `{summary.associated_domains_count}` | Reverse WHOIS & Organization correlation |")
38
+ lines.append(f"| **Open Ports & Services** | `{summary.open_ports_count}` | Exposed internet-facing services |")
39
+ lines.append(f"| **Identified Vulnerabilities (CVEs)** | `{summary.vulnerabilities_count}` | Public CVE references |")
40
+ lines.append(f"| 🚨 **CISA Known Exploited (KEV)** | `{summary.cisa_kev_count}` | **Confirmed actively exploited in the wild** |")
41
+ lines.append(f"| 💥 **Public Exploits & PoCs** | `{summary.exploits_count}` | ExploitDB entries and GitHub PoCs |")
42
+ lines.append("")
43
+
44
+ # 3. Critical Threat Intelligence Alerts
45
+ cisa_kev_vulns = []
46
+ for h in result.hosts:
47
+ for v in h.vulnerabilities:
48
+ if v.in_cisa_kev:
49
+ cisa_kev_vulns.append((h.ip, v))
50
+ for f in result.findings:
51
+ if f.vulnerability and f.vulnerability.in_cisa_kev and not any(v.cve_id == f.vulnerability.cve_id for _, v in cisa_kev_vulns):
52
+ cisa_kev_vulns.append((f.host_ip or result.target, f.vulnerability))
53
+
54
+ if cisa_kev_vulns:
55
+ lines.append("## 2. ⚠️ Critical Risk Highlights (CISA KEV)")
56
+ lines.append("The following vulnerabilities are cataloged by CISA as actively exploited in cyberattacks:")
57
+ lines.append("")
58
+ for host_ip, v in cisa_kev_vulns:
59
+ kev = v.cisa_kev
60
+ lines.append(f"- **[{v.cve_id}](https://nvd.nist.gov/vuln/detail/{v.cve_id})** on `{host_ip}` - *{kev.vulnerability_name if kev else 'Exploited Vulnerability'}*")
61
+ if kev and kev.date_added:
62
+ lines.append(f" - Added to KEV: `{kev.date_added}` | Due Date: `{kev.due_date or 'N/A'}`")
63
+ if kev and kev.required_action:
64
+ lines.append(f" - Action: {kev.required_action}")
65
+ if kev and kev.known_ransomware_campaign_use:
66
+ lines.append(f" - Ransomware Usage: `{kev.known_ransomware_campaign_use}`")
67
+ lines.append("")
68
+
69
+ # 4. Domain & DNS Surface
70
+ subdomains = [f for f in result.findings if f.type == FindingType.SUBDOMAIN and not f.host_ip]
71
+ assoc_domains = [f for f in result.findings if f.type == FindingType.ASSOCIATED_DOMAIN and not f.host_ip]
72
+
73
+ if subdomains or assoc_domains:
74
+ lines.append("## 3. Domain Surface & Correlation")
75
+ lines.append("")
76
+ if subdomains:
77
+ lines.append(f"### Subdomains ({len(subdomains)} identified)")
78
+ lines.append("")
79
+ lines.append("| Subdomain | Discovery Source |")
80
+ lines.append("| :--- | :--- |")
81
+ for sf in subdomains:
82
+ lines.append(f"| `{sf.value}` | {sf.source} |")
83
+ lines.append("")
84
+
85
+ if assoc_domains:
86
+ lines.append(f"### Associated Domains / Reverse WHOIS ({len(assoc_domains)} identified)")
87
+ lines.append("")
88
+ lines.append("| Correlated Domain | Discovery Source |")
89
+ lines.append("| :--- | :--- |")
90
+ for adf in assoc_domains:
91
+ lines.append(f"| `{adf.value}` | {adf.source} |")
92
+ lines.append("")
93
+
94
+ # 5. Per-Host Dossiers & Infrastructure
95
+ if result.hosts:
96
+ lines.append("## 4. Host Intelligence & Vulnerability Dossiers")
97
+ lines.append("")
98
+
99
+ for host in result.hosts:
100
+ loc_parts = [p for p in [host.country_name, host.city, host.region_code] if p]
101
+ loc_str = ", ".join(loc_parts) if loc_parts else "N/A"
102
+ org_str = f"{host.org or host.isp or 'N/A'}"
103
+ if host.asn:
104
+ org_str += f" ({host.asn})"
105
+
106
+ lines.append(f"### 🖥️ Host: `{host.ip}`")
107
+ lines.append(f"- **Organization / ISP:** {org_str}")
108
+ lines.append(f"- **Location:** {loc_str}")
109
+ lines.append(f"- **Operating System:** {host.os or 'N/A'}")
110
+ if host.hostnames:
111
+ lines.append(f"- **Hostnames:** `{', '.join(host.hostnames)}`")
112
+ if host.domains:
113
+ lines.append(f"- **Domains:** `{', '.join(host.domains)}`")
114
+ lines.append("")
115
+
116
+ # Open Ports on this Host
117
+ if host.ports:
118
+ lines.append("#### Exposed Ports & Services")
119
+ lines.append("")
120
+ lines.append("| Port / Proto | Service / Product | Version | Endpoint Link | Source |")
121
+ lines.append("| :---: | :--- | :---: | :--- | :--- |")
122
+ for p in sorted(host.ports, key=lambda x: x.port):
123
+ prod = p.product or p.service or "Unknown"
124
+ ver = p.version or "-"
125
+ link = f"[{p.url}]({p.url})" if p.url else "-"
126
+ src_str = p.source if p.sources else (host.source or "-")
127
+ lines.append(f"| `{p.port}/{p.transport.upper()}` | {prod} | {ver} | {link} | {src_str} |")
128
+ lines.append("")
129
+
130
+ # Vulnerabilities specifically affecting this host
131
+ if host.vulnerabilities:
132
+ lines.append(f"#### Vulnerabilities on `{host.ip}` ({len(host.vulnerabilities)} CVEs)")
133
+ lines.append("")
134
+ lines.append("| CVE ID | CWE Name | CVSS Score | Severity | EPSS Risk | CISA KEV | Exploits / PoCs |")
135
+ lines.append("| :--- | :--- | :---: | :---: | :---: | :---: | :---: |")
136
+
137
+ for v in host.vulnerabilities:
138
+ cvss_str = f"**{v.cvss_score}** (v{v.cvss_version})" if v.cvss_score else "N/A"
139
+ sev_val = v.cvss_severity.value if hasattr(v.cvss_severity, "value") else str(v.cvss_severity)
140
+ sev_badge = f"`{sev_val}`"
141
+ epss_str = f"{v.epss.epss_score * 100:.2f}% (p{v.epss.epss_percentile * 100:.0f})" if v.epss else "N/A"
142
+ kev_str = "🚨 **YES**" if v.in_cisa_kev else "No"
143
+ exp_count = f"**{len(v.exploits)} PoCs**" if v.exploits else "0"
144
+ cwe_str = v.cwe_name or v.cwe_id or "N/A"
145
+
146
+ lines.append(f"| [{v.cve_id}](https://nvd.nist.gov/vuln/detail/{v.cve_id}) | {cwe_str} | {cvss_str} | {sev_badge} | {epss_str} | {kev_str} | {exp_count} |")
147
+ lines.append("")
148
+
149
+ # Exploits for this host
150
+ host_exploits = []
151
+ for v in host.vulnerabilities:
152
+ for exp in v.exploits:
153
+ host_exploits.append((v.cve_id, exp))
154
+
155
+ if host_exploits:
156
+ lines.append(f"##### Exploits & PoCs for `{host.ip}`")
157
+ lines.append("")
158
+ lines.append("| CVE ID | Exploit / PoC Title | Source | URL Link |")
159
+ lines.append("| :--- | :--- | :--- | :--- |")
160
+ for cve, exp in host_exploits:
161
+ lines.append(f"| `{cve}` | {exp.title} | `{exp.source}` | [{exp.url}]({exp.url}) |")
162
+ lines.append("")
163
+ else:
164
+ lines.append("*No CVEs identified on this host.*")
165
+ lines.append("")
166
+
167
+ lines.append("---")
168
+ lines.append("")
169
+
170
+ elif result.target_type == "cve":
171
+ # Standalone CVE scan
172
+ vuln_findings = [f for f in result.findings if f.type == FindingType.VULNERABILITY and f.vulnerability]
173
+ if vuln_findings:
174
+ lines.append("## 4. Vulnerability & Threat Intelligence")
175
+ lines.append("")
176
+ lines.append("| CVE ID | CWE Name | CVSS Score | Severity | EPSS Risk | CISA KEV | Exploits / PoCs |")
177
+ lines.append("| :--- | :--- | :---: | :---: | :---: | :---: | :---: |")
178
+ for vf in vuln_findings:
179
+ v = vf.vulnerability
180
+ cvss_str = f"**{v.cvss_score}** (v{v.cvss_version})" if v.cvss_score else "N/A"
181
+ sev_val = v.cvss_severity.value if hasattr(v.cvss_severity, "value") else str(v.cvss_severity)
182
+ sev_badge = f"`{sev_val}`"
183
+ epss_str = f"{v.epss.epss_score * 100:.2f}%" if v.epss else "N/A"
184
+ kev_str = "🚨 **YES**" if v.in_cisa_kev else "No"
185
+ exp_count = f"**{len(v.exploits)} PoCs**" if v.exploits else "0"
186
+ cwe_str = v.cwe_name or v.cwe_id or "N/A"
187
+ lines.append(f"| [{v.cve_id}](https://nvd.nist.gov/vuln/detail/{v.cve_id}) | {cwe_str} | {cvss_str} | {sev_badge} | {epss_str} | {kev_str} | {exp_count} |")
188
+ lines.append("")
189
+
190
+ lines.append("---")
191
+ lines.append("*Generated automatically by DetecTI-CLI v2.0 — Modern EASM & Threat Intelligence Engine.* ")
192
+ lines.append("*Powered by [DetecTI Security](https://detecti.com.br)*")
193
+
194
+ return "\n".join(lines)
195
+
196
+ @classmethod
197
+ def save(cls, result: ScanResult, output_path: Path | str) -> Path:
198
+ """Save formatted Markdown report to disk."""
199
+ path = Path(output_path)
200
+ path.parent.mkdir(parents=True, exist_ok=True)
201
+ content = cls.generate(result)
202
+ path.write_text(content, encoding="utf-8")
203
+ return path
@@ -0,0 +1 @@
1
+ """Utilities package for ThreatTrack."""