exploitgraph 1.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 (42) hide show
  1. core/__init__.py +0 -0
  2. core/attack_graph.py +83 -0
  3. core/aws_client.py +284 -0
  4. core/config.py +83 -0
  5. core/console.py +469 -0
  6. core/context_engine.py +172 -0
  7. core/correlator.py +476 -0
  8. core/http_client.py +243 -0
  9. core/logger.py +97 -0
  10. core/module_loader.py +69 -0
  11. core/risk_engine.py +47 -0
  12. core/session_manager.py +254 -0
  13. exploitgraph-1.0.0.dist-info/METADATA +429 -0
  14. exploitgraph-1.0.0.dist-info/RECORD +42 -0
  15. exploitgraph-1.0.0.dist-info/WHEEL +5 -0
  16. exploitgraph-1.0.0.dist-info/entry_points.txt +2 -0
  17. exploitgraph-1.0.0.dist-info/licenses/LICENSE +21 -0
  18. exploitgraph-1.0.0.dist-info/top_level.txt +2 -0
  19. modules/__init__.py +0 -0
  20. modules/base.py +82 -0
  21. modules/cloud/__init__.py +0 -0
  22. modules/cloud/aws_credential_validator.py +340 -0
  23. modules/cloud/azure_enum.py +289 -0
  24. modules/cloud/cloudtrail_analyzer.py +494 -0
  25. modules/cloud/gcp_enum.py +272 -0
  26. modules/cloud/iam_enum.py +321 -0
  27. modules/cloud/iam_privilege_escalation.py +515 -0
  28. modules/cloud/metadata_check.py +315 -0
  29. modules/cloud/s3_enum.py +469 -0
  30. modules/discovery/__init__.py +0 -0
  31. modules/discovery/http_enum.py +235 -0
  32. modules/discovery/subdomain_enum.py +260 -0
  33. modules/exploitation/__init__.py +0 -0
  34. modules/exploitation/api_exploit.py +403 -0
  35. modules/exploitation/jwt_attack.py +346 -0
  36. modules/exploitation/ssrf_scanner.py +258 -0
  37. modules/reporting/__init__.py +0 -0
  38. modules/reporting/html_report.py +446 -0
  39. modules/reporting/json_export.py +107 -0
  40. modules/secrets/__init__.py +0 -0
  41. modules/secrets/file_secrets.py +358 -0
  42. modules/secrets/git_secrets.py +267 -0
@@ -0,0 +1,446 @@
1
+ """
2
+ ExploitGraph Module: HTML Report Generator
3
+ Category: reporting
4
+ Generates a professional security assessment HTML report with:
5
+ - D3.js interactive attack graph
6
+ - CVSS-scored findings (expandable)
7
+ - MITRE ATT&CK mapping
8
+ - AWS remediation commands
9
+ - Risk score breakdown
10
+ """
11
+ from __future__ import annotations
12
+ import os
13
+ import json
14
+ import datetime
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from modules.base import BaseModule, ModuleResult
19
+
20
+ if TYPE_CHECKING:
21
+ from core.session_manager import Session
22
+
23
+ SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
24
+ CVSS_COLOR = {"CRITICAL": "#dc2626", "HIGH": "#ea580c",
25
+ "MEDIUM": "#d97706", "LOW": "#16a34a", "INFO": "#6b7280"}
26
+
27
+ MITRE_REF = {
28
+ "T1595": ("T1595", "Active Scanning", "Adversaries scan for target information prior to attack."),
29
+ "T1595.003": ("T1595.003", "Wordlist Scanning", "Scan for target-related files/paths using wordlists."),
30
+ "T1580": ("T1580", "Cloud Infrastructure Discovery", "Query cloud APIs to enumerate infrastructure."),
31
+ "T1530": ("T1530", "Data from Cloud Storage", "Access data from misconfigured cloud storage objects."),
32
+ "T1552.001": ("T1552.001", "Credentials in Files", "Search files for credentials, API keys, and tokens."),
33
+ "T1552.004": ("T1552.004", "Private Keys", "Search for private key material in files."),
34
+ "T1552.005": ("T1552.005", "Cloud Instance Metadata", "Query cloud metadata endpoints for credentials."),
35
+ "T1078": ("T1078", "Valid Accounts", "Use legitimate credentials for unauthorized access."),
36
+ "T1078.004": ("T1078.004", "Cloud Accounts", "Abuse valid cloud account credentials."),
37
+ "T1548": ("T1548", "Abuse Elevation Control", "Abuse cloud IAM misconfigs to escalate privileges."),
38
+ "T1550.001": ("T1550.001", "Application Access Token", "Use forged or stolen application tokens."),
39
+ "T1069.003": ("T1069.003", "Cloud Groups", "Enumerate cloud IAM groups and permissions."),
40
+ "T1110.001": ("T1110.001", "Password Guessing", "Attempt to guess valid credentials."),
41
+ }
42
+
43
+ HTML_TEMPLATE = """<!DOCTYPE html>
44
+ <html lang="en">
45
+ <head>
46
+ <meta charset="UTF-8">
47
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
48
+ <title>ExploitGraph Report — {session_id}</title>
49
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
50
+ <style>
51
+ :root{{--red:#dc2626;--orange:#ea580c;--amber:#d97706;--green:#16a34a;--blue:#2563eb;--purple:#7c3aed;--dark:#0f172a;--card:#1e293b;--border:#334155;--text:#e2e8f0;--muted:#94a3b8}}
52
+ *{{margin:0;padding:0;box-sizing:border-box}}
53
+ body{{background:#0f172a;color:#e2e8f0;font-family:'Segoe UI',system-ui,sans-serif;font-size:14px;line-height:1.6}}
54
+ .container{{max-width:1200px;margin:0 auto;padding:0 24px}}
55
+ .grid-4{{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}}
56
+ .grid-3{{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}}
57
+ .header{{background:#0a0f1a;border-bottom:2px solid #dc2626;padding:32px 0}}
58
+ .logo{{font-size:28px;font-weight:900;color:#dc2626;letter-spacing:-1px}}
59
+ .logo span{{color:#e2e8f0}}
60
+ .header-inner{{display:flex;justify-content:space-between;align-items:center}}
61
+ .badge-conf{{display:inline-block;background:#dc2626;color:#fff;font-size:11px;font-weight:700;padding:2px 10px;border-radius:2px;letter-spacing:2px;margin-top:8px}}
62
+ .killchain{{background:#1e293b;border:1px solid #334155;border-left:4px solid #dc2626;padding:18px 20px;margin:20px 0;border-radius:4px}}
63
+ .chain-steps{{display:flex;align-items:center;flex-wrap:wrap;gap:0;margin-top:10px}}
64
+ .chain-step{{background:#0f172a;border:1px solid #334155;border-radius:6px;padding:10px 14px;text-align:center;min-width:130px}}
65
+ .chain-step.active{{border-color:#dc2626}}
66
+ .chain-step .cs-title{{font-size:11px;font-weight:700;color:#dc2626}}
67
+ .chain-step .cs-sub{{font-size:10px;color:#94a3b8}}
68
+ .chain-arrow{{color:#dc2626;font-size:18px;padding:0 6px;font-weight:700}}
69
+ section{{padding:28px 0}}
70
+ .section-title{{font-size:16px;font-weight:700;padding-bottom:10px;border-bottom:1px solid #334155;margin-bottom:16px}}
71
+ .stat-card{{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:18px}}
72
+ .stat-card .sl{{font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px}}
73
+ .stat-card .sv{{font-size:32px;font-weight:900;margin:4px 0}}
74
+ .stat-card .sd{{font-size:12px;color:#94a3b8}}
75
+ .sc{{border-top:3px solid #dc2626}}.sc .sv{{color:#dc2626}}
76
+ .sh{{border-top:3px solid #ea580c}}.sh .sv{{color:#ea580c}}
77
+ .sm{{border-top:3px solid #d97706}}.sm .sv{{color:#d97706}}
78
+ .si{{border-top:3px solid #2563eb}}.si .sv{{color:#2563eb}}
79
+ #graph-container{{background:#0a0f1a;border:1px solid #334155;border-radius:8px;overflow:hidden}}
80
+ #graph-svg{{width:100%;height:480px}}
81
+ .graph-legend{{display:flex;gap:14px;padding:10px 18px;border-top:1px solid #334155;flex-wrap:wrap}}
82
+ .legend-item{{display:flex;align-items:center;gap:5px;font-size:12px;color:#94a3b8}}
83
+ .legend-dot{{width:10px;height:10px;border-radius:50%}}
84
+ .finding{{background:#1e293b;border:1px solid #334155;border-radius:8px;margin-bottom:12px;overflow:hidden}}
85
+ .fh{{padding:14px 18px;display:flex;justify-content:space-between;align-items:center;cursor:pointer}}
86
+ .fh:hover{{background:#263348}}
87
+ .ft{{font-size:14px;font-weight:700}}
88
+ .fb{{padding:0 18px 16px;display:none}}
89
+ .fb.open{{display:block}}
90
+ .fb h4{{color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin:12px 0 5px}}
91
+ .fb p,.fb pre{{font-size:13px;color:#cbd5e1;line-height:1.7}}
92
+ .fb pre{{background:#0f172a;border:1px solid #334155;border-radius:4px;padding:10px;font-family:monospace;font-size:12px;white-space:pre-wrap}}
93
+ .aws-box{{background:#0c1a2e;border:1px solid #1e3a5f;border-radius:4px;padding:10px;margin-top:8px}}
94
+ .aws-box .al{{color:#60a5fa;font-size:11px;font-weight:700;text-transform:uppercase}}
95
+ .aws-box p{{color:#93c5fd;font-size:13px;margin-top:3px}}
96
+ .badge{{display:inline-block;padding:2px 9px;border-radius:4px;font-size:11px;font-weight:700}}
97
+ .bC{{background:#7f1d1d;color:#fca5a5}}.bH{{background:#7c2d12;color:#fdba74}}
98
+ .bM{{background:#78350f;color:#fcd34d}}.bL{{background:#14532d;color:#86efac}}
99
+ .bI{{background:#1e3a5f;color:#93c5fd}}
100
+ .cvss-bar{{height:5px;background:#334155;border-radius:3px;margin-top:6px}}
101
+ .cvss-fill{{height:100%;border-radius:3px}}
102
+ .mitre-tag{{display:inline-block;background:#1e1b4b;border:1px solid #4338ca;color:#a5b4fc;font-size:11px;padding:2px 7px;border-radius:3px;font-family:monospace;margin-top:5px}}
103
+ .rec-list{{list-style:none;margin-top:5px}}
104
+ .rec-list li{{padding:3px 0 3px 14px;position:relative;color:#cbd5e1;font-size:13px}}
105
+ .rec-list li::before{{content:"→";position:absolute;left:0;color:#2563eb}}
106
+ .table-wrap{{overflow-x:auto}}
107
+ table{{width:100%;border-collapse:collapse}}
108
+ th{{background:#0f172a;color:#94a3b8;font-size:11px;text-transform:uppercase;letter-spacing:1px;padding:9px 12px;text-align:left;border-bottom:1px solid #334155}}
109
+ td{{padding:9px 12px;border-bottom:1px solid #1e293b;font-size:13px;vertical-align:top}}
110
+ tr:hover td{{background:#1a2336}}
111
+ .mono{{font-family:monospace;font-size:12px;word-break:break-all}}
112
+ .mitre-card{{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:14px}}
113
+ .mitre-card .mc-id{{font-size:11px;color:#a5b4fc;font-family:monospace}}
114
+ .mitre-card .mc-name{{font-size:13px;font-weight:700;margin:3px 0}}
115
+ .mitre-card .mc-desc{{font-size:12px;color:#94a3b8}}
116
+ .mitre-card a{{color:#60a5fa;font-size:11px}}
117
+ .rem-block{{background:#0c1a0c;border:1px solid #166534;border-radius:8px;padding:18px;margin-bottom:14px}}
118
+ .rem-block h4{{color:#86efac;font-size:13px;font-weight:700;margin-bottom:8px}}
119
+ .rem-block code{{background:#0f172a;border:1px solid #334155;border-radius:3px;padding:7px 10px;display:block;font-family:monospace;font-size:12px;color:#a3e635;margin:6px 0;white-space:pre-wrap}}
120
+ .risk-score{{font-size:48px;font-weight:900;text-align:center;padding:16px}}
121
+ .footer{{background:#0a0f1a;border-top:1px solid #334155;padding:20px 0;margin-top:32px;text-align:center;color:#475569;font-size:12px}}
122
+ ::-webkit-scrollbar{{width:5px;height:5px}}
123
+ ::-webkit-scrollbar-track{{background:#0f172a}}
124
+ ::-webkit-scrollbar-thumb{{background:#334155;border-radius:3px}}
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="header"><div class="container"><div class="header-inner">
129
+ <div>
130
+ <div class="logo">Exploit<span>Graph</span></div>
131
+ <div style="color:#94a3b8;font-size:12px;margin-top:3px">Automated Attack Path Discovery & Exploitation Framework</div>
132
+ </div>
133
+ <div style="text-align:right">
134
+ <div style="font-size:12px;color:#94a3b8">Security Assessment Report</div>
135
+ <div style="font-weight:700">{session_id}</div>
136
+ <div style="color:#94a3b8;font-size:12px">{report_date}</div>
137
+ <div style="color:#94a3b8;font-size:12px">Target: {target}</div>
138
+ <div class="badge-conf">CONFIDENTIAL</div>
139
+ </div>
140
+ </div></div></div>
141
+
142
+ <div class="container">
143
+ <div class="killchain">
144
+ <div style="color:#dc2626;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:2px">Attack Kill Chain</div>
145
+ <div class="chain-steps">
146
+ <div class="chain-step active"><div class="cs-title">Cloud Misconfig</div><div class="cs-sub">Public storage</div></div>
147
+ <div class="chain-arrow">→</div>
148
+ <div class="chain-step active"><div class="cs-title">Data Exposure</div><div class="cs-sub">Files/backups</div></div>
149
+ <div class="chain-arrow">→</div>
150
+ <div class="chain-step active"><div class="cs-title">Secret Leakage</div><div class="cs-sub">Keys / tokens</div></div>
151
+ <div class="chain-arrow">→</div>
152
+ <div class="chain-step active"><div class="cs-title">API Abuse</div><div class="cs-sub">Credential use</div></div>
153
+ <div class="chain-arrow">→</div>
154
+ <div class="chain-step active"><div class="cs-title">Compromise</div><div class="cs-sub">Full access</div></div>
155
+ </div>
156
+ </div>
157
+
158
+ <section>
159
+ <div class="section-title">Executive Summary</div>
160
+ <div class="grid-4">
161
+ <div class="stat-card sc"><div class="sl">Critical</div><div class="sv">{sev_critical}</div><div class="sd">Immediate action</div></div>
162
+ <div class="stat-card sh"><div class="sl">High</div><div class="sv">{sev_high}</div><div class="sd">Remediate 24h</div></div>
163
+ <div class="stat-card sm"><div class="sl">Secrets Found</div><div class="sv">{secrets_count}</div><div class="sd">Credentials leaked</div></div>
164
+ <div class="stat-card si"><div class="sl">Risk Score</div><div class="sv">{risk_score}</div><div class="sd">{risk_label} severity</div></div>
165
+ </div>
166
+ </section>
167
+
168
+ <section>
169
+ <div class="section-title">Attack Path Graph</div>
170
+ <div id="graph-container">
171
+ <svg id="graph-svg"></svg>
172
+ <div class="graph-legend">
173
+ <div class="legend-item"><div class="legend-dot" style="background:#dc2626"></div>Critical node</div>
174
+ <div class="legend-item"><div class="legend-dot" style="background:#ea580c"></div>High node</div>
175
+ <div class="legend-item"><div class="legend-dot" style="background:#2563eb"></div>Info node</div>
176
+ <div class="legend-item"><div class="legend-dot" style="background:#7c3aed"></div>Module</div>
177
+ <div class="legend-item" style="margin-left:auto;font-size:11px">Drag nodes · Scroll to zoom</div>
178
+ </div>
179
+ </div>
180
+ </section>
181
+
182
+ <section>
183
+ <div class="section-title">Security Findings ({total_findings})</div>
184
+ {findings_html}
185
+ </section>
186
+
187
+ <section>
188
+ <div class="section-title">Extracted Secrets ({secrets_count})</div>
189
+ <div class="table-wrap"><table>
190
+ <thead><tr><th>#</th><th>Type</th><th>Severity</th><th>Value</th><th>Source</th><th>AWS Parallel</th></tr></thead>
191
+ <tbody>{secrets_html}</tbody>
192
+ </table></div>
193
+ </section>
194
+
195
+ <section>
196
+ <div class="section-title">MITRE ATT&CK for Cloud</div>
197
+ <div class="grid-3">{mitre_html}</div>
198
+ </section>
199
+
200
+ <section>
201
+ <div class="section-title">AWS Remediation Commands</div>
202
+ <div class="rem-block"><h4>1. Enable S3 Block Public Access</h4>
203
+ <code>aws s3api put-public-access-block --bucket BUCKET_NAME \\
204
+ --public-access-block-configuration \\
205
+ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"</code>
206
+ </div>
207
+ <div class="rem-block"><h4>2. Migrate Secrets to AWS Secrets Manager</h4>
208
+ <code>aws secretsmanager create-secret --name "prod/app/credentials" \\
209
+ --secret-string '{{"api_key":"NEW_VALUE"}}' \\
210
+ --kms-key-id alias/aws/secretsmanager</code>
211
+ </div>
212
+ <div class="rem-block"><h4>3. Enable CloudTrail</h4>
213
+ <code>aws cloudtrail create-trail --name app-trail --s3-bucket-name my-cloudtrail-logs \\
214
+ --is-multi-region-trail --enable-log-file-validation
215
+ aws cloudtrail start-logging --name app-trail</code>
216
+ </div>
217
+ <div class="rem-block"><h4>4. Enable GuardDuty</h4>
218
+ <code>aws guardduty create-detector --enable \\
219
+ --finding-publishing-frequency FIFTEEN_MINUTES</code>
220
+ </div>
221
+ <div class="rem-block"><h4>5. Rotate Compromised IAM Credentials</h4>
222
+ <code>aws iam update-access-key --access-key-id AKIAXXXXXXXX --status Inactive
223
+ aws iam create-access-key --user-name SERVICE_ACCOUNT</code>
224
+ </div>
225
+ <div class="rem-block"><h4>6. Enable IMDSv2 on EC2 (Prevent SSRF)</h4>
226
+ <code>aws ec2 modify-instance-metadata-options --instance-id i-XXXXXXXXX \\
227
+ --http-tokens required --http-put-response-hop-limit 1</code>
228
+ </div>
229
+ </section>
230
+
231
+ <section>
232
+ <div class="section-title">Exploitation Evidence</div>
233
+ <div class="table-wrap"><table>
234
+ <thead><tr><th>#</th><th>Module</th><th>Action</th><th>Result</th><th>Detail</th><th>Time</th></tr></thead>
235
+ <tbody>{exploits_html}</tbody>
236
+ </table></div>
237
+ </section>
238
+
239
+ </div>
240
+ <div class="footer"><div class="container">
241
+ <p><strong style="color:#dc2626">ExploitGraph</strong> — Automated Attack Path Discovery & Exploitation Framework</p>
242
+ <p style="margin-top:3px">For authorized security research only. github.com/prajwalpawar/ExploitGraph</p>
243
+ </div></div>
244
+
245
+ <script>
246
+ const graphData={graph_json};
247
+ (function(){{
248
+ const svg=d3.select("#graph-svg");
249
+ const w=document.getElementById("graph-svg").clientWidth||900,h=480;
250
+ svg.attr("viewBox",`0 0 ${{w}} ${{h}}`);
251
+ const colorMap={{CRITICAL:"#dc2626",HIGH:"#ea580c",INFO:"#2563eb",attacker:"#dc2626",target:"#2563eb",exposure:"#ea580c",secret:"#d97706",exploit:"#dc2626",compromise:"#7f1d1d",module:"#7c3aed",asset:"#6b7280",credential:"#d97706",access:"#16a34a"}};
252
+ function nc(n){{return colorMap[n.type]||colorMap[n.severity]||"#6b7280"}}
253
+ const nodes=(graphData.nodes||[]).map(n=>({{...n}}));
254
+ const links=(graphData.edges||[]).map(e=>({{source:e.source,target:e.target,label:e.label||"",technique:e.technique||""}}));
255
+ svg.append("defs").append("marker").attr("id","ah").attr("viewBox","0 0 10 10").attr("refX",22).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M0,0 L10,5 L0,10 Z").attr("fill","#dc2626");
256
+ const sim=d3.forceSimulation(nodes).force("link",d3.forceLink(links).id(d=>d.id).distance(150).strength(0.8)).force("charge",d3.forceManyBody().strength(-400)).force("center",d3.forceCenter(w/2,h/2)).force("collision",d3.forceCollide(50));
257
+ const g=svg.append("g");
258
+ svg.call(d3.zoom().scaleExtent([0.2,4]).on("zoom",e=>g.attr("transform",e.transform)));
259
+ const link=g.append("g").selectAll("line").data(links).join("line").attr("stroke","#dc2626").attr("stroke-opacity",0.5).attr("stroke-width",1.5).attr("marker-end","url(#ah)");
260
+ const ll=g.append("g").selectAll("text").data(links).join("text").text(d=>d.technique||"").attr("font-size",9).attr("fill","#475569").attr("text-anchor","middle");
261
+ const node=g.append("g").selectAll("g").data(nodes).join("g").attr("cursor","pointer").call(d3.drag().on("start",(e,d)=>{{if(!e.active)sim.alphaTarget(0.3).restart();d.fx=d.x;d.fy=d.y}}).on("drag",(e,d)=>{{d.fx=e.x;d.fy=e.y}}).on("end",(e,d)=>{{if(!e.active)sim.alphaTarget(0);d.fx=null;d.fy=null}}));
262
+ node.append("circle").attr("r",26).attr("fill",d=>nc(d)+"33").attr("stroke",d=>nc(d)).attr("stroke-width",2);
263
+ node.append("text").text(d=>(d.label||d.id).split("\\n")[0]).attr("text-anchor","middle").attr("dy",-4).attr("fill","#e2e8f0").attr("font-size",11).attr("font-weight","600");
264
+ node.append("text").text(d=>(d.label||"").split("\\n")[1]||"").attr("text-anchor","middle").attr("dy",10).attr("fill","#94a3b8").attr("font-size",9);
265
+ const tip=d3.select("body").append("div").style("position","fixed").style("background","#1e293b").style("border","1px solid #334155").style("border-radius","6px").style("padding","8px 12px").style("font-size","12px").style("color","#e2e8f0").style("pointer-events","none").style("opacity",0).style("max-width","220px").style("z-index","9999");
266
+ node.on("mouseover",(e,d)=>tip.style("opacity",1).html(`<strong style="color:#dc2626">${{d.id}}</strong><br>${{d.details||d.label||""}}`)).on("mousemove",e=>tip.style("left",(e.clientX+10)+"px").style("top",(e.clientY-24)+"px")).on("mouseout",()=>tip.style("opacity",0));
267
+ sim.on("tick",()=>{{link.attr("x1",d=>d.source.x).attr("y1",d=>d.source.y).attr("x2",d=>d.target.x).attr("y2",d=>d.target.y);ll.attr("x",d=>(d.source.x+d.target.x)/2).attr("y",d=>(d.source.y+d.target.y)/2-6);node.attr("transform",d=>`translate(${{d.x}},${{d.y}})`)}});
268
+ }})();
269
+ function toggleF(h){{const b=h.nextElementSibling;b.classList.toggle("open");h.querySelector(".fa").textContent=b.classList.contains("open")?"▲":"▼"}}
270
+ </script>
271
+ </body></html>"""
272
+
273
+
274
+ class HtmlReport(BaseModule):
275
+
276
+ NAME = "html_report"
277
+ DESCRIPTION = "Generate a professional HTML security assessment report with interactive D3.js attack graph"
278
+ AUTHOR = "ExploitGraph Team"
279
+ VERSION = "1.2.0"
280
+ CATEGORY = "reporting"
281
+ SEVERITY = "INFO"
282
+ MITRE = []
283
+ AWS_PARALLEL = "AWS Security Hub consolidated findings export"
284
+
285
+ OPTIONS = {
286
+ "OUTPUT_DIR": {"default": "reports", "required": False, "description": "Output directory for reports"},
287
+ "OPEN": {"default": "false", "required": False, "description": "Auto-open in browser after generation"},
288
+ }
289
+
290
+ def run(self, session: "Session") -> ModuleResult:
291
+ from core.logger import log
292
+ from core.risk_engine import risk_engine
293
+ from core.attack_graph import attack_graph
294
+
295
+ self._timer_start()
296
+ log.section("HTML Report Generator")
297
+
298
+ output_dir = Path(self.get_option("OUTPUT_DIR", "reports"))
299
+ output_dir.mkdir(exist_ok=True)
300
+ report_path = output_dir / f"ExploitGraph_{session.session_id}.html"
301
+ json_path = output_dir / f"ExploitGraph_{session.session_id}.json"
302
+
303
+ # Build attack graph
304
+ attack_graph.build(session)
305
+ graph_json = json.dumps(attack_graph.to_json())
306
+
307
+ # Risk scoring
308
+ score, label = risk_engine.session_score(session)
309
+ session.update_risk(score)
310
+
311
+ # Sort findings
312
+ sorted_findings = sorted(session.findings,
313
+ key=lambda f: SEVERITY_ORDER.get(f.get("severity", "INFO"), 99))
314
+
315
+ sev = session.summary()["severity"]
316
+
317
+ html = HTML_TEMPLATE.format(
318
+ session_id = session.session_id,
319
+ report_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M UTC"),
320
+ target = session.target,
321
+ sev_critical = sev["CRITICAL"],
322
+ sev_high = sev["HIGH"],
323
+ secrets_count = len(session.secrets),
324
+ risk_score = score,
325
+ risk_label = label,
326
+ total_findings = len(sorted_findings),
327
+ findings_html = self._render_findings(sorted_findings),
328
+ secrets_html = self._render_secrets(session.secrets),
329
+ mitre_html = self._render_mitre(session),
330
+ exploits_html = self._render_exploits(session.exploit_results),
331
+ graph_json = graph_json,
332
+ )
333
+
334
+ report_path.write_text(html, encoding="utf-8")
335
+
336
+ # JSON export
337
+ json_path.write_text(json.dumps({
338
+ "session": session.summary(),
339
+ "findings": sorted_findings,
340
+ "secrets": session.secrets,
341
+ "exploits": session.exploit_results,
342
+ "graph": attack_graph.to_json(),
343
+ "risk": risk_engine.score_breakdown(session),
344
+ }, indent=2), encoding="utf-8")
345
+
346
+ log.success(f"HTML report: {report_path}")
347
+ log.success(f"JSON export: {json_path}")
348
+ log.info(f"Risk Score: {score}/10 [{label}]")
349
+ log.info(f"Open: firefox {report_path} &")
350
+
351
+ if self.get_option("OPEN", "false").lower() == "true":
352
+ import subprocess
353
+ subprocess.Popen(["xdg-open", str(report_path)],
354
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
355
+
356
+ elapsed = self._timer_stop()
357
+ return ModuleResult(True, {
358
+ "report_path": str(report_path),
359
+ "json_path": str(json_path),
360
+ "findings": len(sorted_findings),
361
+ "risk_score": score,
362
+ })
363
+
364
+ def _render_findings(self, findings: list) -> str:
365
+ if not findings:
366
+ return '<p style="color:#94a3b8">No findings recorded.</p>'
367
+ html = ""
368
+ for i, f in enumerate(findings, 1):
369
+ sev = f.get("severity", "INFO")
370
+ cvss = f.get("cvss_score", 0)
371
+ color = CVSS_COLOR.get(sev, "#6b7280")
372
+ mitre = f.get("mitre_technique", "")
373
+ recs = f.get("recommendation", "")
374
+ rec_items = "".join(f"<li>{l.strip().lstrip('0123456789. ')}</li>"
375
+ for l in recs.split("\n") if l.strip())
376
+ aws = f.get("aws_parallel", "")
377
+ html += f"""<div class="finding">
378
+ <div class="fh" onclick="toggleF(this)">
379
+ <div><div class="ft">{i}. {f.get('title','')}</div>
380
+ {'<span class="mitre-tag">'+mitre+'</span>' if mitre else ''}</div>
381
+ <div style="display:flex;align-items:center;gap:10px">
382
+ <div style="text-align:right"><div style="font-size:10px;color:#94a3b8">CVSS</div>
383
+ <div style="font-size:16px;font-weight:900;color:{color}">{cvss}</div></div>
384
+ <span class="badge b{sev[0]}">{sev}</span>
385
+ <span class="fa" style="color:#94a3b8">▼</span>
386
+ </div>
387
+ </div>
388
+ <div class="fb">
389
+ <div class="cvss-bar"><div class="cvss-fill" style="width:{int(cvss/10*100)}%;background:{color}"></div></div>
390
+ <h4>Description</h4><p>{f.get('description','')}</p>
391
+ <h4>Evidence</h4><pre>{f.get('evidence','')}</pre>
392
+ {'<div class="aws-box"><div class="al">AWS Real-World Parallel</div><p>'+aws+'</p></div>' if aws else ''}
393
+ <h4>Remediation</h4><ul class="rec-list">{rec_items}</ul>
394
+ </div></div>"""
395
+ return html
396
+
397
+ def _render_secrets(self, secrets: list) -> str:
398
+ html = ""
399
+ for i, s in enumerate(secrets, 1):
400
+ sev = s.get("severity", "HIGH")
401
+ val = s.get("value", "")
402
+ html += f"""<tr>
403
+ <td>{i}</td>
404
+ <td><code class="mono">{s.get('secret_type','')}</code></td>
405
+ <td><span class="badge b{sev[0]}">{sev}</span></td>
406
+ <td class="mono">{val[:45]}{'...' if len(val)>45 else ''}</td>
407
+ <td style="color:#94a3b8;font-size:12px">{s.get('source','')[:50]}</td>
408
+ <td style="color:#60a5fa;font-size:12px">{s.get('aws_parallel','')[:70]}</td>
409
+ </tr>"""
410
+ return html or "<tr><td colspan='6' style='color:#94a3b8'>No secrets found</td></tr>"
411
+
412
+ def _render_mitre(self, session: "Session") -> str:
413
+ used = set()
414
+ for f in session.findings:
415
+ t = f.get("mitre_technique", "")
416
+ for key in MITRE_REF:
417
+ if key in t:
418
+ used.add(key)
419
+ html = ""
420
+ for key in sorted(used):
421
+ if key in MITRE_REF:
422
+ t_id, t_name, t_desc = MITRE_REF[key]
423
+ url = f"https://attack.mitre.org/techniques/{t_id.replace('.','/')}"
424
+ html += f"""<div class="mitre-card">
425
+ <div class="mc-id">{t_id}</div>
426
+ <div class="mc-name">{t_name}</div>
427
+ <div class="mc-desc">{t_desc}</div>
428
+ <a href="{url}" target="_blank">→ MITRE ATT&CK</a>
429
+ </div>"""
430
+ return html or '<p style="color:#94a3b8">No MITRE techniques recorded.</p>'
431
+
432
+ def _render_exploits(self, results: list) -> str:
433
+ html = ""
434
+ for i, r in enumerate(results, 1):
435
+ ok = r.get("success", False)
436
+ cls = "bC" if ok else "bL"
437
+ lbl = "SUCCESS" if ok else "FAILED"
438
+ html += f"""<tr>
439
+ <td>{i}</td>
440
+ <td style="font-size:12px;color:#94a3b8">{r.get('module','')[:25]}</td>
441
+ <td style="font-weight:600">{r.get('action','')[:55]}</td>
442
+ <td><span class="badge {cls}">{lbl}</span></td>
443
+ <td style="font-size:12px;color:#94a3b8">{r.get('detail','')[:70]}</td>
444
+ <td style="font-size:11px;color:#475569;white-space:nowrap">{r.get('timestamp','')[:19]}</td>
445
+ </tr>"""
446
+ return html or "<tr><td colspan='6' style='color:#94a3b8'>No exploitation attempts</td></tr>"
@@ -0,0 +1,107 @@
1
+ """
2
+ ExploitGraph Module: JSON Exporter
3
+ Category: reporting
4
+ Exports session data as structured JSON — for CI/CD integration, SIEM ingestion,
5
+ custom tooling, or feeding into other security platforms.
6
+ """
7
+ from __future__ import annotations
8
+ import json
9
+ import datetime
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+
13
+ from modules.base import BaseModule, ModuleResult
14
+
15
+ if TYPE_CHECKING:
16
+ from core.session_manager import Session
17
+
18
+
19
+ class JsonExport(BaseModule):
20
+
21
+ NAME = "json_export"
22
+ DESCRIPTION = "Export all session findings, secrets, and attack graph as structured JSON"
23
+ AUTHOR = "ExploitGraph Team"
24
+ VERSION = "1.0.0"
25
+ CATEGORY = "reporting"
26
+ SEVERITY = "INFO"
27
+ MITRE = []
28
+ AWS_PARALLEL = "AWS Security Hub findings JSON format / ASFF export"
29
+
30
+ OPTIONS = {
31
+ "OUTPUT_DIR": {"default": "reports", "required": False, "description": "Output directory"},
32
+ "PRETTY": {"default": "true", "required": False, "description": "Pretty-print JSON"},
33
+ "INCLUDE_GRAPH": {"default": "true", "required": False, "description": "Include attack graph in export"},
34
+ }
35
+
36
+ def run(self, session: "Session") -> ModuleResult:
37
+ from core.logger import log
38
+ from core.risk_engine import risk_engine
39
+ from core.attack_graph import attack_graph
40
+
41
+ self._timer_start()
42
+ output_dir = Path(self.get_option("OUTPUT_DIR", "reports"))
43
+ output_dir.mkdir(exist_ok=True)
44
+
45
+ attack_graph.build(session)
46
+ score, label = risk_engine.session_score(session)
47
+
48
+ indent = 2 if self.get_option("PRETTY", "true").lower() == "true" else None
49
+
50
+ # Full export
51
+ full_export = {
52
+ "meta": {
53
+ "tool": "ExploitGraph",
54
+ "version": "1.0.0",
55
+ "session_id": session.session_id,
56
+ "target": session.target,
57
+ "exported": datetime.datetime.now(datetime.timezone.utc).isoformat() + "Z",
58
+ "mode": session.mode,
59
+ },
60
+ "risk": {
61
+ "score": score,
62
+ "label": label,
63
+ "breakdown": risk_engine.score_breakdown(session),
64
+ },
65
+ "summary": session.summary(),
66
+ "findings": session.findings,
67
+ "secrets": session.secrets,
68
+ "endpoints": session.endpoints,
69
+ "exploit_results": session.exploit_results,
70
+ }
71
+
72
+ if self.get_option("INCLUDE_GRAPH", "true").lower() == "true":
73
+ full_export["attack_graph"] = attack_graph.to_json()
74
+
75
+ out_path = output_dir / f"ExploitGraph_{session.session_id}_full.json"
76
+ out_path.write_text(json.dumps(full_export, indent=indent), encoding="utf-8")
77
+ log.success(f"JSON export: {out_path}")
78
+
79
+ # Minimal findings-only export (for SIEM/ticketing)
80
+ findings_export = {
81
+ "session_id": session.session_id,
82
+ "target": session.target,
83
+ "risk_score": score,
84
+ "findings": [
85
+ {
86
+ "id": f"EG-{i+1:04d}",
87
+ "title": f["title"],
88
+ "severity": f["severity"],
89
+ "cvss": f.get("cvss_score", 0),
90
+ "module": f.get("module", ""),
91
+ "mitre": f.get("mitre_technique", ""),
92
+ "aws": f.get("aws_parallel", ""),
93
+ "recommendation": f.get("recommendation", ""),
94
+ }
95
+ for i, f in enumerate(session.findings)
96
+ ],
97
+ }
98
+ min_path = output_dir / f"ExploitGraph_{session.session_id}_findings.json"
99
+ min_path.write_text(json.dumps(findings_export, indent=indent), encoding="utf-8")
100
+ log.success(f"Findings JSON: {min_path}")
101
+
102
+ elapsed = self._timer_stop()
103
+ return ModuleResult(True, {
104
+ "full_export": str(out_path),
105
+ "findings_export": str(min_path),
106
+ "findings": len(session.findings),
107
+ })
File without changes