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.
- detecti/__init__.py +0 -0
- detecti/cli.py +649 -0
- detecti/config.py +188 -0
- detecti/core/__init__.py +1 -0
- detecti/core/database/__init__.py +5 -0
- detecti/core/database/config_db.py +73 -0
- detecti/core/database/schema.py +136 -0
- detecti/core/database/storage.py +1388 -0
- detecti/core/engine.py +1032 -0
- detecti/core/models.py +278 -0
- detecti/data/config.sqlite +0 -0
- detecti/data/dbs/.gitkeep +2 -0
- detecti/data/dbs/example.com.sqlite +0 -0
- detecti/modules/__init__.py +29 -0
- detecti/modules/base.py +57 -0
- detecti/modules/censys.py +813 -0
- detecti/modules/crtsh.py +98 -0
- detecti/modules/exploitdb.py +138 -0
- detecti/modules/masscan.py +561 -0
- detecti/modules/nuclei.py +449 -0
- detecti/modules/nvd.py +300 -0
- detecti/modules/reverse_whois.py +225 -0
- detecti/modules/shodan.py +412 -0
- detecti/reporters/__init__.py +7 -0
- detecti/reporters/csv_reporter.py +74 -0
- detecti/reporters/html_reporter.py +356 -0
- detecti/reporters/json_reporter.py +26 -0
- detecti/reporters/markdown_reporter.py +203 -0
- detecti/utils/__init__.py +1 -0
- detecti/utils/http.py +294 -0
- detecti/utils/logger.py +378 -0
- detecti/utils/setup.py +453 -0
- detecti/web/__init__.py +6 -0
- detecti/web/api/__init__.py +1 -0
- detecti/web/api/auth.py +109 -0
- detecti/web/api/graph_builder.py +901 -0
- detecti/web/api/routes.py +1602 -0
- detecti/web/process_manager.py +283 -0
- detecti/web/server.py +183 -0
- detecti/web/static/android-chrome-192x192.png +0 -0
- detecti/web/static/android-chrome-512x512.png +0 -0
- detecti/web/static/apple-touch-icon.png +0 -0
- detecti/web/static/css/__init__.py +1 -0
- detecti/web/static/css/dashboard.css +3802 -0
- detecti/web/static/favicon-16x16.png +0 -0
- detecti/web/static/favicon-32x32.png +0 -0
- detecti/web/static/favicon.ico +0 -0
- detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
- detecti/web/static/img/detecti-ico.png +0 -0
- detecti/web/static/index.html +677 -0
- detecti/web/static/js/__init__.py +1 -0
- detecti/web/static/js/api.js +177 -0
- detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
- detecti/web/static/js/cytoscape-dagre.js +397 -0
- detecti/web/static/js/cytoscape.min.js +31 -0
- detecti/web/static/js/dagre.min.js +3809 -0
- detecti/web/static/js/graph.js +7439 -0
- detecti/web/static/js/lucide.min.js +12 -0
- detecti/web/static/login.html +290 -0
- detecti/web/static/site.webmanifest +1 -0
- detecti_cli-2.0.0.dist-info/METADATA +554 -0
- detecti_cli-2.0.0.dist-info/RECORD +64 -0
- detecti_cli-2.0.0.dist-info/WHEEL +4 -0
- 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 & Vulnerability Assessment • <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 & 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."""
|