@@ -10,6 +10,21 @@ use tabled::{Table, Tabled, settings::Style};
1010use crate :: cloud:: CloudAssetFinding ;
1111use crate :: social:: SocialIntelligenceSummary ;
1212
13+ pub struct ReconSummary < ' a > {
14+ pub domain : & ' a str ,
15+ pub subs : & ' a HashSet < String > ,
16+ pub header_map : & ' a HashMap < String , ( u16 , Option < String > ) > ,
17+ pub open_ports_map : & ' a HashMap < String , Vec < u16 > > ,
18+ pub cors_map : & ' a HashMap < String , Vec < String > > ,
19+ pub software_map : & ' a HashMap < String , HashMap < String , String > > ,
20+ pub takeover_map : & ' a HashMap < String , Vec < String > > ,
21+ #[ allow( dead_code) ]
22+ pub cloud_saas_map : & ' a HashMap < String , Vec < String > > ,
23+ pub cloud_asset_map : & ' a HashMap < String , Vec < CloudAssetFinding > > ,
24+ pub social_intel : Option < & ' a SocialIntelligenceSummary > ,
25+ pub output_dir : & ' a str ,
26+ }
27+
1328pub struct PrettyReportGenerator ;
1429
1530#[ derive( Tabled ) ]
@@ -30,27 +45,17 @@ struct SubdomainRow {
3045
3146impl PrettyReportGenerator {
3247 pub fn generate_markdown_report (
33- domain : & str ,
34- subs : & HashSet < String > ,
35- header_map : & HashMap < String , ( u16 , Option < String > ) > ,
36- open_ports_map : & HashMap < String , Vec < u16 > > ,
37- cors_map : & HashMap < String , Vec < String > > ,
38- software_map : & HashMap < String , HashMap < String , String > > ,
39- takeover_map : & HashMap < String , Vec < String > > ,
40- _cloud_saas_map : & HashMap < String , Vec < String > > ,
41- cloud_asset_map : & HashMap < String , Vec < CloudAssetFinding > > ,
42- social_intel : Option < & SocialIntelligenceSummary > ,
43- output_dir : & str ,
48+ summary : & ReconSummary ,
4449 ) -> Result < ( ) , Box < dyn std:: error:: Error + Send + Sync > > {
45- let report_file = format ! ( "{}/{}_REPORT.md" , output_dir, domain) ;
50+ let report_file = format ! ( "{}/{}_REPORT.md" , summary . output_dir, summary . domain) ;
4651 let mut f = File :: create ( & report_file) ?;
4752
4853 let timestamp = Local :: now ( ) . format ( "%Y-%m-%d %H:%M:%S %Z" ) ;
4954
5055 // Header
5156 writeln ! ( f, "# ShadowMap Security Report" ) ?;
5257 writeln ! ( f) ?;
53- writeln ! ( f, "**Target Domain:** `{}`" , domain) ?;
58+ writeln ! ( f, "**Target Domain:** `{}`" , summary . domain) ?;
5459 writeln ! ( f, "**Scan Date:** {}" , timestamp) ?;
5560 writeln ! ( f, "**Generated by:** ShadowMap v{}" , env!( "CARGO_PKG_VERSION" ) ) ?;
5661 writeln ! ( f) ?;
@@ -61,10 +66,10 @@ impl PrettyReportGenerator {
6166 writeln ! ( f, "## Executive Summary" ) ?;
6267 writeln ! ( f) ?;
6368
64- let total_subs = subs. len ( ) ;
65- let cors_issues = cors_map. len ( ) ;
66- let takeover_risks = takeover_map. len ( ) ;
67- let cloud_assets = cloud_asset_map. len ( ) ;
69+ let total_subs = summary . subs . len ( ) ;
70+ let cors_issues = summary . cors_map . len ( ) ;
71+ let takeover_risks = summary . takeover_map . len ( ) ;
72+ let cloud_assets = summary . cloud_asset_map . len ( ) ;
6873
6974 let risk_level = if cors_issues > 0 || takeover_risks > 0 {
7075 "**HIGH**"
@@ -96,7 +101,7 @@ impl PrettyReportGenerator {
96101 writeln ! ( f, "**Impact:** Data theft, unauthorized API access" ) ?;
97102 writeln ! ( f) ?;
98103
99- for ( sub, issues) in cors_map. iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
104+ for ( sub, issues) in summary . cors_map . iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
100105 writeln ! ( f, "**Host:** `{}`" , sub) ?;
101106 for issue in issues {
102107 writeln ! ( f, "- {}" , issue) ?;
@@ -117,7 +122,7 @@ impl PrettyReportGenerator {
117122 if takeover_risks > 0 {
118123 writeln ! ( f, "### Subdomain Takeover Risks" ) ?;
119124 writeln ! ( f) ?;
120- for ( sub, risks) in takeover_map. iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
125+ for ( sub, risks) in summary . takeover_map . iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
121126 writeln ! ( f, "**Subdomain:** `{}`" , sub) ?;
122127 for risk in risks {
123128 writeln ! ( f, "- {}" , risk) ?;
@@ -133,8 +138,8 @@ impl PrettyReportGenerator {
133138 writeln ! ( f, "| # | Subdomain | Status | Server | Open Ports | Issues |" ) ?;
134139 writeln ! ( f, "|---|-----------|--------|--------|------------|--------|" ) ?;
135140
136- for ( idx, sub) in subs. iter ( ) . sorted ( ) . enumerate ( ) {
137- let ( status, server) = header_map. get ( sub) . cloned ( ) . unwrap_or ( ( 0 , None ) ) ;
141+ for ( idx, sub) in summary . subs . iter ( ) . sorted ( ) . enumerate ( ) {
142+ let ( status, server) = summary . header_map . get ( sub) . cloned ( ) . unwrap_or ( ( 0 , None ) ) ;
138143 let status_icon = match status {
139144 200 ..=299 => "OK" ,
140145 300 ..=399 => "REDIRECT" ,
@@ -143,12 +148,12 @@ impl PrettyReportGenerator {
143148 _ => "UNKNOWN" ,
144149 } ;
145150
146- let ports = open_ports_map. get ( sub)
151+ let ports = summary . open_ports_map . get ( sub)
147152 . map ( |v| v. iter ( ) . map ( |p| p. to_string ( ) ) . join ( ", " ) )
148153 . unwrap_or_else ( || "-" . to_string ( ) ) ;
149154
150- let issues_count = cors_map. get ( sub) . map ( |v| v. len ( ) ) . unwrap_or ( 0 )
151- + takeover_map. get ( sub) . map ( |v| v. len ( ) ) . unwrap_or ( 0 ) ;
155+ let issues_count = summary . cors_map . get ( sub) . map ( |v| v. len ( ) ) . unwrap_or ( 0 )
156+ + summary . takeover_map . get ( sub) . map ( |v| v. len ( ) ) . unwrap_or ( 0 ) ;
152157
153158 let issues_display = if issues_count > 0 {
154159 format ! ( "ISSUES: {}" , issues_count)
@@ -174,15 +179,15 @@ impl PrettyReportGenerator {
174179 writeln ! ( f, "## Port Analysis" ) ?;
175180 writeln ! ( f) ?;
176181 let mut all_ports: HashSet < u16 > = HashSet :: new ( ) ;
177- for ports in open_ports_map. values ( ) {
182+ for ports in summary . open_ports_map . values ( ) {
178183 all_ports. extend ( ports) ;
179184 }
180185
181186 writeln ! ( f, "**Unique Open Ports:** {}" , all_ports. iter( ) . sorted( ) . map( |p| p. to_string( ) ) . join( ", " ) ) ?;
182187 writeln ! ( f) ?;
183188
184189 for port in all_ports. iter ( ) . sorted ( ) {
185- let hosts_with_port: Vec < & String > = open_ports_map
190+ let hosts_with_port: Vec < & String > = summary . open_ports_map
186191 . iter ( )
187192 . filter ( |( _, ports) | ports. contains ( port) )
188193 . map ( |( host, _) | host)
@@ -210,11 +215,11 @@ impl PrettyReportGenerator {
210215 }
211216
212217 // Software Fingerprints
213- if !software_map. is_empty ( ) {
218+ if !summary . software_map . is_empty ( ) {
214219 writeln ! ( f, "## Software Fingerprints" ) ?;
215220 writeln ! ( f) ?;
216221
217- for ( sub, fingerprints) in software_map. iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
222+ for ( sub, fingerprints) in summary . software_map . iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
218223 if !fingerprints. is_empty ( ) {
219224 writeln ! ( f, "**{}**" , sub) ?;
220225 for ( key, value) in fingerprints {
@@ -226,14 +231,14 @@ impl PrettyReportGenerator {
226231 }
227232
228233 // Cloud Assets
229- if !cloud_asset_map. is_empty ( ) {
234+ if !summary . cloud_asset_map . is_empty ( ) {
230235 writeln ! ( f, "## Cloud Infrastructure Analysis" ) ?;
231236 writeln ! ( f) ?;
232- writeln ! ( f, "**Total Predicted Assets:** {}" , cloud_asset_map. values( ) . map( |v| v. len( ) ) . sum:: <usize >( ) ) ?;
237+ writeln ! ( f, "**Total Predicted Assets:** {}" , summary . cloud_asset_map. values( ) . map( |v| v. len( ) ) . sum:: <usize >( ) ) ?;
233238 writeln ! ( f) ?;
234239
235240 let mut provider_counts: HashMap < String , usize > = HashMap :: new ( ) ;
236- for assets in cloud_asset_map. values ( ) {
241+ for assets in summary . cloud_asset_map . values ( ) {
237242 for asset in assets {
238243 * provider_counts. entry ( asset. provider . clone ( ) ) . or_insert ( 0 ) += 1 ;
239244 }
@@ -248,7 +253,7 @@ impl PrettyReportGenerator {
248253 }
249254
250255 // Social Intelligence
251- if let Some ( intel) = social_intel {
256+ if let Some ( intel) = summary . social_intel {
252257 writeln ! ( f, "## AI-Powered Threat Intelligence" ) ?;
253258 writeln ! ( f) ?;
254259 writeln ! ( f, "**Framework:** {}" , intel. framework_name) ?;
@@ -305,7 +310,7 @@ impl PrettyReportGenerator {
305310 if takeover_risks > 0 {
306311 writeln ! ( f, "2. **Investigate subdomain takeover risks** on {} subdomain(s)" , takeover_risks) ?;
307312 }
308- if open_ports_map. values ( ) . any ( |ports| ports. contains ( & 22 ) ) {
313+ if summary . open_ports_map . values ( ) . any ( |ports| ports. contains ( & 22 ) ) {
309314 writeln ! ( f, "3. **Review SSH exposure** - Port 22 should not be publicly accessible" ) ?;
310315 }
311316 writeln ! ( f) ?;
@@ -331,56 +336,47 @@ impl PrettyReportGenerator {
331336 Ok ( ( ) )
332337 }
333338
334- pub fn print_terminal_summary (
335- domain : & str ,
336- subs : & HashSet < String > ,
337- header_map : & HashMap < String , ( u16 , Option < String > ) > ,
338- open_ports_map : & HashMap < String , Vec < u16 > > ,
339- cors_map : & HashMap < String , Vec < String > > ,
340- takeover_map : & HashMap < String , Vec < String > > ,
341- cloud_asset_map : & HashMap < String , Vec < CloudAssetFinding > > ,
342- social_intel : Option < & SocialIntelligenceSummary > ,
343- ) {
339+ pub fn print_terminal_summary ( summary : & ReconSummary ) {
344340 println ! ( ) ;
345341 println ! ( "{}" , "═══════════════════════════════════════════════════════════" . bright_cyan( ) ) ;
346- println ! ( "{}" , format!( " SCAN RESULTS: {}" , domain) . bright_white( ) . bold( ) ) ;
342+ println ! ( "{}" , format!( " SCAN RESULTS: {}" , summary . domain) . bright_white( ) . bold( ) ) ;
347343 println ! ( "{}" , "═══════════════════════════════════════════════════════════" . bright_cyan( ) ) ;
348344 println ! ( ) ;
349345
350346 // Quick Stats
351347 println ! ( "{}" , "QUICK STATS" . bright_yellow( ) . bold( ) ) ;
352348 println ! ( "{}" , "─────────────────────────────────────────────────────────" . bright_black( ) ) ;
353- println ! ( " {} Subdomains discovered" , subs. len( ) . to_string( ) . bright_green( ) . bold( ) ) ;
354- println ! ( " {} Live hosts verified" , subs. len( ) . to_string( ) . bright_green( ) . bold( ) ) ;
349+ println ! ( " {} Subdomains discovered" , summary . subs. len( ) . to_string( ) . bright_green( ) . bold( ) ) ;
350+ println ! ( " {} Live hosts verified" , summary . subs. len( ) . to_string( ) . bright_green( ) . bold( ) ) ;
355351
356- if cors_map. is_empty ( ) {
352+ if summary . cors_map . is_empty ( ) {
357353 println ! ( " {} CORS vulnerabilities" , "0" . bright_green( ) . bold( ) ) ;
358354 } else {
359- println ! ( " {} CORS vulnerabilities [WARNING]" , cors_map. len( ) . to_string( ) . bright_red( ) . bold( ) ) ;
355+ println ! ( " {} CORS vulnerabilities [WARNING]" , summary . cors_map. len( ) . to_string( ) . bright_red( ) . bold( ) ) ;
360356 }
361357
362- if takeover_map. is_empty ( ) {
358+ if summary . takeover_map . is_empty ( ) {
363359 println ! ( " {} Takeover risks" , "0" . bright_green( ) . bold( ) ) ;
364360 } else {
365- println ! ( " {} Takeover risks [WARNING]" , takeover_map. len( ) . to_string( ) . bright_yellow( ) . bold( ) ) ;
361+ println ! ( " {} Takeover risks [WARNING]" , summary . takeover_map. len( ) . to_string( ) . bright_yellow( ) . bold( ) ) ;
366362 }
367363
368- println ! ( " {} Cloud assets flagged" , cloud_asset_map. len( ) . to_string( ) . bright_cyan( ) . bold( ) ) ;
364+ println ! ( " {} Cloud assets flagged" , summary . cloud_asset_map. len( ) . to_string( ) . bright_cyan( ) . bold( ) ) ;
369365 println ! ( ) ;
370366
371367 // Critical Findings
372- if !cors_map. is_empty ( ) || !takeover_map. is_empty ( ) {
368+ if !summary . cors_map . is_empty ( ) || !summary . takeover_map . is_empty ( ) {
373369 println ! ( "{}" , "CRITICAL FINDINGS" . bright_red( ) . bold( ) ) ;
374370 println ! ( "{}" , "─────────────────────────────────────────────────────────" . bright_black( ) ) ;
375371
376- for ( sub, issues) in cors_map. iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
372+ for ( sub, issues) in summary . cors_map . iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
377373 println ! ( " {} {}" , "CORS:" . bright_red( ) . bold( ) , sub. bright_white( ) ) ;
378374 for issue in issues {
379375 println ! ( " • {}" , issue. bright_yellow( ) ) ;
380376 }
381377 }
382378
383- for ( sub, risks) in takeover_map. iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
379+ for ( sub, risks) in summary . takeover_map . iter ( ) . sorted_by_key ( |( k, _) | k. as_str ( ) ) {
384380 println ! ( " {} {}" , "TAKEOVER:" . bright_yellow( ) . bold( ) , sub. bright_white( ) ) ;
385381 for risk in risks {
386382 println ! ( " • {}" , risk) ;
@@ -394,25 +390,25 @@ impl PrettyReportGenerator {
394390 println ! ( "{}" , "─────────────────────────────────────────────────────────" . bright_black( ) ) ;
395391
396392 let mut rows: Vec < SubdomainRow > = Vec :: new ( ) ;
397- for sub in subs. iter ( ) . sorted ( ) . take ( 10 ) {
398- let ( status, server) = header_map. get ( sub) . cloned ( ) . unwrap_or ( ( 0 , None ) ) ;
393+ for sub in summary . subs . iter ( ) . sorted ( ) . take ( 10 ) {
394+ let ( status, server) = summary . header_map . get ( sub) . cloned ( ) . unwrap_or ( ( 0 , None ) ) ;
399395 let status_str = if status == 0 {
400396 "N/A" . to_string ( )
401397 } else {
402398 format ! ( "{}" , status)
403399 } ;
404400
405- let ports = open_ports_map. get ( sub)
401+ let ports = summary . open_ports_map . get ( sub)
406402 . map ( |v| v. iter ( ) . take ( 4 ) . map ( |p| p. to_string ( ) ) . join ( "," ) )
407403 . unwrap_or_else ( || "-" . to_string ( ) ) ;
408404
409- let cors_str = if cors_map. contains_key ( sub) {
405+ let cors_str = if summary . cors_map . contains_key ( sub) {
410406 "WARNING" . to_string ( )
411407 } else {
412408 "OK" . to_string ( )
413409 } ;
414410
415- let risks = if takeover_map. contains_key ( sub) {
411+ let risks = if summary . takeover_map . contains_key ( sub) {
416412 "WARNING" . to_string ( )
417413 } else {
418414 "OK" . to_string ( )
@@ -433,14 +429,14 @@ impl PrettyReportGenerator {
433429 println ! ( "{}" , table) ;
434430 }
435431
436- if subs. len ( ) > 10 {
432+ if summary . subs . len ( ) > 10 {
437433 println ! ( ) ;
438- println ! ( " {} more subdomain(s) not shown..." , ( subs. len( ) - 10 ) . to_string( ) . bright_black( ) ) ;
434+ println ! ( " {} more subdomain(s) not shown..." , ( summary . subs. len( ) - 10 ) . to_string( ) . bright_black( ) ) ;
439435 }
440436 println ! ( ) ;
441437
442438 // Social Intelligence Summary
443- if let Some ( intel) = social_intel {
439+ if let Some ( intel) = summary . social_intel {
444440 if intel. metrics . total_signals > 0 {
445441 println ! ( "{}" , "AI THREAT INTELLIGENCE" . bright_magenta( ) . bold( ) ) ;
446442 println ! ( "{}" , "─────────────────────────────────────────────────────────" . bright_black( ) ) ;
0 commit comments