eva-exploit 2.5__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.
modules/attack_map.py ADDED
@@ -0,0 +1,467 @@
1
+ import json
2
+ import os
3
+ import platform
4
+ import re
5
+ import socket
6
+ import webbrowser
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+
11
+ IP_PATTERN = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
12
+ FQDN_PATTERN = re.compile(r"\b[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+){1,}\b")
13
+ NMAP_SERVICE_PATTERN = re.compile(r"(\d{1,5})/tcp\s+open\s+([^\s]+)", re.IGNORECASE)
14
+ DOMAIN_USER_PATTERN = re.compile(r"\b([A-Za-z0-9_.-]+)\\([A-Za-z0-9_.-]+)\b")
15
+ UID_NAME_PATTERN = re.compile(r"uid=\d+\(([^)]+)\)")
16
+
17
+
18
+ def _normalize_ip(ip):
19
+ parts = ip.split(".")
20
+ if len(parts) != 4:
21
+ return None
22
+ try:
23
+ nums = [int(x) for x in parts]
24
+ except ValueError:
25
+ return None
26
+ if any(n < 0 or n > 255 for n in nums):
27
+ return None
28
+ return ip
29
+
30
+
31
+ def _guess_target_os(blob):
32
+ text = blob.lower()
33
+ if any(word in text for word in ["windows", "winrm", "active directory", "smb", "kerberos"]):
34
+ return "Windows"
35
+ if any(word in text for word in ["linux", "ubuntu", "debian", "centos", "kali", "parrot", "openssh"]):
36
+ return "Linux"
37
+ return "Unknown"
38
+
39
+
40
+ def _extract_primary_target(user_lines):
41
+ for line in user_lines:
42
+ for ip in IP_PATTERN.findall(line):
43
+ norm = _normalize_ip(ip)
44
+ if norm:
45
+ return norm
46
+ return None
47
+
48
+
49
+ def _operator_fingerprint():
50
+ hostname = socket.gethostname() or "localhost"
51
+ os_name = platform.system() or "UnknownOS"
52
+ release = platform.release() or "UnknownRelease"
53
+ arch = platform.machine() or "UnknownArch"
54
+ cpu = platform.processor() or platform.uname().processor or "UnknownCPU"
55
+ cores = os.cpu_count() or 0
56
+ user = os.getenv("USER") or os.getenv("USERNAME") or "unknown"
57
+ label = f"Operator ({hostname})"
58
+ detail = f"User: {user} | OS: {os_name} {release} | Arch: {arch} | CPU: {cpu} | Cores: {cores}"
59
+ return label, detail
60
+
61
+
62
+ def _add_node(nodes, node_id, label, node_type, detail=""):
63
+ if node_id in nodes:
64
+ return
65
+ nodes[node_id] = {
66
+ "id": node_id,
67
+ "label": label,
68
+ "type": node_type,
69
+ "detail": detail,
70
+ }
71
+
72
+
73
+ def _add_edge(edges, src, dst, label, detail=""):
74
+ edge_id = f"{src}->{dst}:{label}"
75
+ if edge_id in edges:
76
+ return
77
+ edges[edge_id] = {
78
+ "id": edge_id,
79
+ "source": src,
80
+ "target": dst,
81
+ "label": label,
82
+ "detail": detail,
83
+ }
84
+
85
+
86
+ def build_attack_graph(session_name, timeline):
87
+ nodes = {}
88
+ edges = {}
89
+
90
+ user_lines = [item.get("content", "") for item in timeline if item.get("type") == "user"]
91
+ analysis_lines = [item.get("content", "") for item in timeline if item.get("type") == "analysis"]
92
+ command_items = [item for item in timeline if item.get("type") == "command"]
93
+
94
+ primary_target = _extract_primary_target(user_lines + analysis_lines)
95
+ all_analysis = "\n".join(analysis_lines)
96
+ full_blob = "\n".join(user_lines + analysis_lines + [item.get("cmd", "") + "\n" + item.get("output", "") for item in command_items])
97
+
98
+ operator_id = "operator"
99
+ op_label, op_detail = _operator_fingerprint()
100
+ _add_node(nodes, operator_id, op_label, "operator", op_detail)
101
+
102
+ if primary_target:
103
+ target_id = f"target:{primary_target}"
104
+ target_os = _guess_target_os("\n".join(user_lines) + "\n" + all_analysis)
105
+ _add_node(nodes, target_id, f"Target {primary_target}", "target", f"OS guess: {target_os}")
106
+ _add_edge(edges, operator_id, target_id, "engages", "Initial assessment scope")
107
+
108
+ # Add hosts explicitly observed in session artifacts.
109
+ for ip in IP_PATTERN.findall(full_blob):
110
+ norm = _normalize_ip(ip)
111
+ if not norm:
112
+ continue
113
+ if primary_target and norm == primary_target:
114
+ continue
115
+ ip_id = f"host:{norm}"
116
+ _add_node(nodes, ip_id, norm, "host", "Observed in chat/analysis/command artifacts")
117
+ _add_edge(edges, operator_id, ip_id, "observes", "Seen in session artifacts")
118
+
119
+ for fqdn in FQDN_PATTERN.findall("\n".join(user_lines + analysis_lines)):
120
+ if fqdn.count(".") < 1:
121
+ continue
122
+ domain_id = f"domain:{fqdn.lower()}"
123
+ _add_node(nodes, domain_id, fqdn.lower(), "domain", "Observed DNS/FQDN artifact")
124
+ if primary_target:
125
+ _add_edge(edges, f"target:{primary_target}", domain_id, "resolves", "Target/DNS relationship")
126
+
127
+ for item in command_items:
128
+ cmd = item.get("cmd", "")
129
+ out = item.get("output", "")
130
+
131
+ mentioned_ips = []
132
+ for ip in IP_PATTERN.findall(cmd + "\n" + out):
133
+ norm = _normalize_ip(ip)
134
+ if norm:
135
+ mentioned_ips.append(norm)
136
+
137
+ for ip in mentioned_ips:
138
+ ip_id = f"host:{ip}"
139
+ _add_node(nodes, ip_id, ip, "host", "Host seen in command/output")
140
+ _add_edge(edges, operator_id, ip_id, "scans", cmd[:80])
141
+
142
+ for port, service in NMAP_SERVICE_PATTERN.findall(out):
143
+ if mentioned_ips:
144
+ host = mentioned_ips[0]
145
+ else:
146
+ host = primary_target
147
+ if not host:
148
+ continue
149
+
150
+ host_id = f"host:{host}" if host != primary_target else f"target:{host}"
151
+ service_id = f"svc:{host}:{port}:{service.lower()}"
152
+ _add_node(
153
+ nodes,
154
+ service_id,
155
+ f"{service}/{port}",
156
+ "service",
157
+ f"Service {service} open on TCP/{port}",
158
+ )
159
+ _add_edge(edges, host_id, service_id, "exposes", f"Derived from `{cmd}`")
160
+
161
+ for domain, user in DOMAIN_USER_PATTERN.findall(out):
162
+ domain_id = f"domain:{domain.lower()}"
163
+ user_id = f"user:{domain.lower()}\\{user.lower()}"
164
+ _add_node(nodes, domain_id, domain, "domain", "Domain observed in output")
165
+ _add_node(nodes, user_id, f"{domain}\\{user}", "user", "Domain account observed")
166
+ _add_edge(edges, domain_id, user_id, "contains", "Domain principal relationship")
167
+ if primary_target:
168
+ _add_edge(edges, f"target:{primary_target}", user_id, "auth-context", "Credential/artifact linkage")
169
+
170
+ for username in UID_NAME_PATTERN.findall(out):
171
+ user_id = f"user:local:{username.lower()}"
172
+ _add_node(nodes, user_id, username, "user", "Linux local account artifact")
173
+ if primary_target:
174
+ _add_edge(edges, f"target:{primary_target}", user_id, "local-user", "uid() account extraction")
175
+
176
+ whoami = out.strip().splitlines()
177
+ if len(whoami) == 1 and 0 < len(whoami[0]) <= 32 and " " not in whoami[0]:
178
+ if re.match(r"^[A-Za-z0-9_.\\$-]+$", whoami[0]):
179
+ user_text = whoami[0]
180
+ user_id = f"user:observed:{user_text.lower()}"
181
+ _add_node(nodes, user_id, user_text, "user", "Observed identity output")
182
+ if primary_target:
183
+ _add_edge(edges, f"target:{primary_target}", user_id, "execution-context", f"From `{cmd}`")
184
+
185
+ graph = {
186
+ "meta": {
187
+ "session": session_name,
188
+ "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC"),
189
+ "node_count": len(nodes),
190
+ "edge_count": len(edges),
191
+ },
192
+ "nodes": list(nodes.values()),
193
+ "edges": list(edges.values()),
194
+ }
195
+ # If no actionable artifact was discovered, keep only the operator node.
196
+ if len(nodes) == 1:
197
+ graph["edges"] = []
198
+ graph["meta"]["edge_count"] = 0
199
+ graph["meta"]["node_count"] = 1
200
+ return graph
201
+
202
+
203
+ def _html_template(graph_json, version="unknown"):
204
+ return f"""<!doctype html>
205
+ <html lang=\"en\">
206
+ <head>
207
+ <meta charset=\"utf-8\" />
208
+ <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
209
+ <title>EVA Attack Map</title>
210
+ <style>
211
+ :root {{
212
+ --bg:#0b1324;
213
+ --panel:#111d34;
214
+ --panel2:#162645;
215
+ --line:#2e4167;
216
+ --text:#e6eefc;
217
+ --muted:#96a8cc;
218
+ --operator:#2dd4bf;
219
+ --target:#f97316;
220
+ --host:#60a5fa;
221
+ --service:#c084fc;
222
+ --user:#22c55e;
223
+ --domain:#facc15;
224
+ --other:#94a3b8;
225
+ }}
226
+ * {{ box-sizing: border-box; }}
227
+ body {{ margin:0; font-family:"JetBrains Mono","Consolas",monospace; background:radial-gradient(circle at 20% 0%,#172643,#0b1324 60%); color:var(--text); }}
228
+ .top {{ display:flex; gap:12px; align-items:center; justify-content:space-between; padding:14px 16px; border-bottom:1px solid var(--line); background:rgba(10,16,31,.8); backdrop-filter: blur(8px); position:sticky; top:0; z-index:3; }}
229
+ .title {{ font-size:14px; letter-spacing:.08em; text-transform:uppercase; color:#cde0ff; }}
230
+ .meta {{ color:var(--muted); font-size:12px; }}
231
+ .layout {{ display:grid; grid-template-columns: 1fr 300px; min-height: calc(100vh - 58px); }}
232
+ .canvas-wrap {{ position:relative; overflow:hidden; }}
233
+ svg {{ width:100%; height:100%; display:block; cursor:grab; }}
234
+ .node {{ cursor:pointer; }}
235
+ .node text {{ font-size:11px; fill:var(--text); pointer-events:none; }}
236
+ .node circle {{ stroke:#d9e4ff; stroke-opacity:.35; stroke-width:1.2; }}
237
+ .edge {{ stroke:#8ba5d9; stroke-opacity:.45; stroke-width:1.1; }}
238
+ .edge-label {{ fill:#a6bbe4; font-size:10px; }}
239
+ .side {{ border-left:1px solid var(--line); padding:14px; background:linear-gradient(180deg,var(--panel),var(--panel2)); }}
240
+ .card {{ border:1px solid #22355a; border-radius:10px; padding:10px; margin-bottom:12px; background:rgba(12,20,37,.6); }}
241
+ .label {{ font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; margin-bottom:4px; }}
242
+ .value {{ font-size:13px; line-height:1.4; word-break:break-word; }}
243
+ .legend-item {{ display:flex; align-items:center; gap:8px; font-size:12px; margin:6px 0; }}
244
+ .dot {{ width:10px; height:10px; border-radius:999px; border:1px solid rgba(255,255,255,.4); }}
245
+ .footer {{ color:var(--muted); text-align:center; font-size:12px; padding:10px 14px 16px; border-top:1px solid var(--line); background:rgba(10,16,31,.8); }}
246
+ @media (max-width:900px) {{ .layout {{ grid-template-columns:1fr; }} .side {{ border-left:none; border-top:1px solid var(--line); }} }}
247
+ </style>
248
+ </head>
249
+ <body>
250
+ <div class=\"top\">
251
+ <div class=\"title\">EVA Visual Attack Map</div>
252
+ <div class=\"meta\" id=\"meta\"></div>
253
+ </div>
254
+ <div class=\"layout\">
255
+ <div class=\"canvas-wrap\">
256
+ <svg id=\"map\" viewBox=\"0 0 1280 840\" preserveAspectRatio=\"xMidYMid meet\"></svg>
257
+ </div>
258
+ <aside class=\"side\">
259
+ <div class=\"card\">
260
+ <div class=\"label\">Selection</div>
261
+ <div class=\"value\" id=\"sel-title\">None</div>
262
+ <div class=\"label\" style=\"margin-top:8px;\">Details</div>
263
+ <div class=\"value\" id=\"sel-detail\">Click a node or edge.</div>
264
+ </div>
265
+ <div class=\"card\">
266
+ <div class=\"label\">Legend</div>
267
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--operator)\"></span>Operator</div>
268
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--target)\"></span>Target</div>
269
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--host)\"></span>Host</div>
270
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--service)\"></span>Service</div>
271
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--user)\"></span>User</div>
272
+ <div class=\"legend-item\"><span class=\"dot\" style=\"background:var(--domain)\"></span>Domain</div>
273
+ </div>
274
+ <div class=\"card\">
275
+ <div class=\"label\">Navigation</div>
276
+ <div class=\"value\">Drag to pan, wheel to zoom, click node for intel.</div>
277
+ </div>
278
+ </aside>
279
+ </div>
280
+ <div class=\"footer\">EVA v{version}<br/>Made by: Arcangelo</div>
281
+
282
+ <script>
283
+ const graph = {graph_json};
284
+ const svg = document.getElementById('map');
285
+ const NS = 'http://www.w3.org/2000/svg';
286
+ const meta = document.getElementById('meta');
287
+ const selTitle = document.getElementById('sel-title');
288
+ const selDetail = document.getElementById('sel-detail');
289
+
290
+ meta.textContent = `${{graph.meta.session}} | ${{graph.meta.generated_at}} | Nodes: ${{graph.meta.node_count}} | Edges: ${{graph.meta.edge_count}} | Made by Arcangelo`;
291
+
292
+ const typeColor = {{
293
+ operator: getComputedStyle(document.documentElement).getPropertyValue('--operator').trim(),
294
+ target: getComputedStyle(document.documentElement).getPropertyValue('--target').trim(),
295
+ host: getComputedStyle(document.documentElement).getPropertyValue('--host').trim(),
296
+ service: getComputedStyle(document.documentElement).getPropertyValue('--service').trim(),
297
+ user: getComputedStyle(document.documentElement).getPropertyValue('--user').trim(),
298
+ domain: getComputedStyle(document.documentElement).getPropertyValue('--domain').trim(),
299
+ other: getComputedStyle(document.documentElement).getPropertyValue('--other').trim(),
300
+ }};
301
+
302
+ const positions = new Map();
303
+ const cx = 640, cy = 420, radius = 300;
304
+ const nodes = graph.nodes;
305
+ nodes.forEach((n, i) => {{
306
+ const ang = (Math.PI * 2 * i) / Math.max(nodes.length, 1);
307
+ positions.set(n.id, {{ x: cx + Math.cos(ang) * radius, y: cy + Math.sin(ang) * radius }});
308
+ }});
309
+
310
+ const gRoot = document.createElementNS(NS, 'g');
311
+ svg.appendChild(gRoot);
312
+ const gEdges = document.createElementNS(NS, 'g');
313
+ const gNodes = document.createElementNS(NS, 'g');
314
+ gRoot.appendChild(gEdges);
315
+ gRoot.appendChild(gNodes);
316
+
317
+ function edgeMid(a, b) {{
318
+ return {{ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }};
319
+ }}
320
+
321
+ function draw() {{
322
+ gEdges.innerHTML = '';
323
+ gNodes.innerHTML = '';
324
+
325
+ graph.edges.forEach(e => {{
326
+ const a = positions.get(e.source);
327
+ const b = positions.get(e.target);
328
+ if (!a || !b) return;
329
+
330
+ const line = document.createElementNS(NS, 'line');
331
+ line.setAttribute('x1', a.x); line.setAttribute('y1', a.y);
332
+ line.setAttribute('x2', b.x); line.setAttribute('y2', b.y);
333
+ line.setAttribute('class', 'edge');
334
+ line.addEventListener('click', () => {{
335
+ selTitle.textContent = `${{e.label}}`;
336
+ selDetail.textContent = e.detail || `${{e.source}} -> ${{e.target}}`;
337
+ }});
338
+ gEdges.appendChild(line);
339
+
340
+ const mid = edgeMid(a, b);
341
+ const txt = document.createElementNS(NS, 'text');
342
+ txt.setAttribute('x', mid.x + 4);
343
+ txt.setAttribute('y', mid.y - 4);
344
+ txt.setAttribute('class', 'edge-label');
345
+ txt.textContent = e.label;
346
+ gEdges.appendChild(txt);
347
+ }});
348
+
349
+ graph.nodes.forEach(n => {{
350
+ const p = positions.get(n.id);
351
+ const group = document.createElementNS(NS, 'g');
352
+ group.setAttribute('class', 'node');
353
+
354
+ const circle = document.createElementNS(NS, 'circle');
355
+ circle.setAttribute('cx', p.x);
356
+ circle.setAttribute('cy', p.y);
357
+ circle.setAttribute('r', n.type === 'target' ? 16 : 13);
358
+ circle.setAttribute('fill', typeColor[n.type] || typeColor.other);
359
+ group.appendChild(circle);
360
+
361
+ const text = document.createElementNS(NS, 'text');
362
+ text.setAttribute('x', p.x + 16);
363
+ text.setAttribute('y', p.y + 4);
364
+ text.textContent = n.label;
365
+ group.appendChild(text);
366
+
367
+ group.addEventListener('click', () => {{
368
+ selTitle.textContent = `${{n.label}} [${{n.type}}]`;
369
+ selDetail.textContent = n.detail || 'No details';
370
+ }});
371
+
372
+ enableDrag(group, n.id);
373
+ gNodes.appendChild(group);
374
+ }});
375
+ }}
376
+
377
+ function svgPoint(clientX, clientY) {{
378
+ const pt = svg.createSVGPoint();
379
+ pt.x = clientX;
380
+ pt.y = clientY;
381
+ return pt.matrixTransform(svg.getScreenCTM().inverse());
382
+ }}
383
+
384
+ let dragNode = null;
385
+ function enableDrag(group, nodeId) {{
386
+ group.addEventListener('mousedown', (ev) => {{
387
+ dragNode = nodeId;
388
+ ev.stopPropagation();
389
+ }});
390
+ }}
391
+
392
+ window.addEventListener('mousemove', (ev) => {{
393
+ if (!dragNode) return;
394
+ const p = svgPoint(ev.clientX, ev.clientY);
395
+ positions.set(dragNode, {{ x: p.x, y: p.y }});
396
+ draw();
397
+ }});
398
+ window.addEventListener('mouseup', () => dragNode = null);
399
+
400
+ let view = {{ x: 0, y: 0, w: 1280, h: 840 }};
401
+ let panning = false;
402
+ let panStart = null;
403
+ svg.addEventListener('mousedown', (ev) => {{
404
+ if (dragNode) return;
405
+ if (ev.button !== 0) return;
406
+ panning = true;
407
+ panStart = {{ mx: ev.clientX, my: ev.clientY, vx: view.x, vy: view.y, vw: view.w, vh: view.h }};
408
+ svg.style.cursor = 'grabbing';
409
+ }});
410
+ window.addEventListener('mouseup', () => {{
411
+ panning = false;
412
+ svg.style.cursor = 'grab';
413
+ }});
414
+ window.addEventListener('mousemove', (ev) => {{
415
+ if (!panning) return;
416
+ const panFactor = 0.35;
417
+ const dx = (ev.clientX - panStart.mx) * (panStart.vw / svg.clientWidth) * panFactor;
418
+ const dy = (ev.clientY - panStart.my) * (panStart.vh / svg.clientHeight) * panFactor;
419
+ view.x = panStart.vx - dx;
420
+ view.y = panStart.vy - dy;
421
+ svg.setAttribute('viewBox', `${{view.x}} ${{view.y}} ${{view.w}} ${{view.h}}`);
422
+ }});
423
+
424
+ svg.addEventListener('wheel', (ev) => {{
425
+ ev.preventDefault();
426
+ const scale = ev.deltaY < 0 ? 0.97 : 1.03;
427
+ const oldW = view.w;
428
+ const oldH = view.h;
429
+ const minW = 520, maxW = 4200;
430
+ const minH = 340, maxH = 2800;
431
+ view.w = Math.min(maxW, Math.max(minW, view.w * scale));
432
+ view.h = Math.min(maxH, Math.max(minH, view.h * scale));
433
+ // Keep zoom centered around current view center to avoid jumps.
434
+ view.x += (oldW - view.w) / 2;
435
+ view.y += (oldH - view.h) / 2;
436
+ svg.setAttribute('viewBox', `${{view.x}} ${{view.y}} ${{view.w}} ${{view.h}}`);
437
+ }}, {{ passive:false }});
438
+
439
+ draw();
440
+ </script>
441
+ </body>
442
+ </html>
443
+ """
444
+
445
+
446
+ def generate_attack_map_files(session_name, timeline, maps_dir, version="unknown"):
447
+ maps_dir = Path(maps_dir)
448
+ maps_dir.mkdir(parents=True, exist_ok=True)
449
+
450
+ graph = build_attack_graph(session_name, timeline)
451
+ ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
452
+ base = f"{session_name}_attack_map_{ts}"
453
+
454
+ json_path = maps_dir / f"{base}.json"
455
+ html_path = maps_dir / f"{base}.html"
456
+
457
+ json_path.write_text(json.dumps(graph, indent=2), encoding="utf-8")
458
+ html_path.write_text(_html_template(json.dumps(graph), version=version), encoding="utf-8")
459
+
460
+ return html_path, json_path, graph
461
+
462
+
463
+ def open_attack_map(path):
464
+ file_path = Path(path).expanduser().resolve()
465
+ if not file_path.exists():
466
+ return False
467
+ return webbrowser.open(file_path.as_uri())