11use clap:: { Parser , Subcommand } ;
22use colored:: * ;
33use std:: fmt;
4+ use std:: fs;
45use std:: io:: { self , Write } ;
5- use std:: path:: PathBuf ;
6+ use std:: path:: { Path , PathBuf } ;
7+ use chrono:: Utc ;
8+ use serde_json:: json;
69
710use crate :: { run, Args , BoxError } ;
811
@@ -98,6 +101,7 @@ const SIMULATED_VULNERABILITIES: &[SimulatedVulnerability] = &[
98101 summary : "Verbose logging can expose bearer tokens in debug mode." ,
99102 } ,
100103] ;
104+
101105// ============================================================================
102106// CLI STRUCTURE - Elegant User Experience
103107// ============================================================================
@@ -567,6 +571,9 @@ impl ShadowMapCLI {
567571 TerminalUI :: show_progress ( "Resolving transitive dependencies" ) ;
568572 TerminalUI :: show_progress ( & format ! ( "Generating {} SBOM" , format) ) ;
569573
574+ let sbom_payload = Self :: render_sbom_stub ( & manifest, & format) ;
575+ Self :: write_stub_file ( & output, & sbom_payload) ?;
576+
570577 TerminalUI :: print_success ( & format ! ( "SBOM generated: {}" , output. display( ) ) ) ;
571578 TerminalUI :: print_section_end ( ) ;
572579
@@ -585,7 +592,8 @@ impl ShadowMapCLI {
585592 TerminalUI :: show_progress ( "Loading SBOM" ) ;
586593 TerminalUI :: show_progress ( "Querying vulnerability databases" ) ;
587594 TerminalUI :: show_progress ( "Analyzing CVE matches" ) ;
588-
595+
596+
589597 let vulnerabilities = SIMULATED_VULNERABILITIES ;
590598 let ( critical, high, medium, low) = Self :: count_vulnerabilities ( vulnerabilities) ;
591599
@@ -615,6 +623,9 @@ impl ShadowMapCLI {
615623 TerminalUI :: show_progress ( & format ! ( "Generating {} report" , format. to_uppercase( ) ) ) ;
616624 TerminalUI :: show_progress ( "Checking compliance standards" ) ;
617625
626+ let report_payload = Self :: render_report_stub ( & format, SIMULATED_VULNERABILITIES ) ;
627+ Self :: write_stub_file ( & output, & report_payload) ?;
628+
618629 TerminalUI :: print_success ( & format ! ( "Report generated: {}" , output. display( ) ) ) ;
619630 TerminalUI :: print_info ( "✓ EO 14028 compliant" ) ;
620631 TerminalUI :: print_info ( "✓ NIS2 compliant" ) ;
@@ -651,11 +662,16 @@ impl ShadowMapCLI {
651662 if let Some ( threshold) = & fail_on {
652663 TerminalUI :: print_info ( & format ! ( "Fail threshold: {}" , threshold) ) ;
653664 }
665+
666+ fs:: create_dir_all ( & output_dir) . map_err ( |err| -> BoxError { Box :: new ( err) } ) ?;
654667
655668 // Step 1: Generate SBOM
656669 TerminalUI :: print_step ( 1 , 3 , "Generate SBOM" ) ;
657670 TerminalUI :: show_progress ( "Analyzing project dependencies" ) ;
658- TerminalUI :: print_success ( "SBOM created" ) ;
671+ let sbom_path = output_dir. join ( "sbom.json" ) ;
672+ let sbom_payload = Self :: render_sbom_stub ( & manifest, "cyclonedx" ) ;
673+ Self :: write_stub_file ( & sbom_path, & sbom_payload) ?;
674+ TerminalUI :: print_success ( & format ! ( "SBOM created: {}" , sbom_path. display( ) ) ) ;
659675
660676 // Step 2: Vulnerability Scan
661677 TerminalUI :: print_step ( 2 , 3 , "Vulnerability Scan" ) ;
@@ -664,6 +680,13 @@ impl ShadowMapCLI {
664680 let ( critical, high, medium, low) = Self :: count_vulnerabilities ( vulnerabilities) ;
665681 TerminalUI :: print_vulnerability_summary ( critical, high, medium, low) ;
666682 TerminalUI :: print_vulnerability_table ( vulnerabilities) ;
683+ let scan_results_path = output_dir. join ( "scan-results.json" ) ;
684+ let scan_payload = Self :: render_scan_results_stub ( vulnerabilities) ;
685+ Self :: write_stub_file ( & scan_results_path, & scan_payload) ?;
686+ TerminalUI :: print_info ( & format ! (
687+ "Scan results saved: {}" ,
688+ scan_results_path. display( )
689+ ) ) ;
667690
668691 if let Some ( threshold) = & fail_on {
669692 if Self :: should_fail ( threshold, critical, high, medium, low) {
@@ -679,7 +702,10 @@ impl ShadowMapCLI {
679702 // Step 3: Generate Reports
680703 TerminalUI :: print_step ( 3 , 3 , "Generate Reports" ) ;
681704 TerminalUI :: show_progress ( "Creating compliance reports" ) ;
682- TerminalUI :: print_success ( & format ! ( "Reports saved to {}/" , output_dir. display( ) ) ) ;
705+ let report_path = output_dir. join ( "report.json" ) ;
706+ let report_payload = Self :: render_report_stub ( "json" , vulnerabilities) ;
707+ Self :: write_stub_file ( & report_path, & report_payload) ?;
708+ TerminalUI :: print_success ( & format ! ( "Report saved: {}" , report_path. display( ) ) ) ;
683709
684710 println ! ( ) ;
685711 println ! ( "{}" , "═" . repeat( 70 ) . bright_green( ) ) ;
@@ -785,6 +811,8 @@ impl ShadowMapCLI {
785811 }
786812
787813 // Helper functions
814+
815+
788816 fn should_fail ( threshold : & str , critical : u32 , high : u32 , medium : u32 , low : u32 ) -> bool {
789817 match threshold. to_lowercase ( ) . as_str ( ) {
790818 "critical" => critical > 0 ,
@@ -809,4 +837,130 @@ impl ShadowMapCLI {
809837
810838 counts
811839 }
840+
841+ fn render_sbom_stub ( manifest : & Path , format : & str ) -> String {
842+ let sbom = json ! ( {
843+ "format" : format,
844+ "manifest" : manifest. display( ) . to_string( ) ,
845+ "generated_at" : Utc :: now( ) . to_rfc3339( ) ,
846+ "components" : [
847+ {
848+ "name" : "shadowmap" ,
849+ "version" : VERSION ,
850+ "type" : "application" ,
851+ "licenses" : [ "Apache-2.0" , "MIT" ] ,
852+ } ,
853+ {
854+ "name" : "openssl" ,
855+ "version" : "3.2.1" ,
856+ "type" : "library" ,
857+ "licenses" : [ "Apache-2.0" ] ,
858+ } ,
859+ {
860+ "name" : "tokio" ,
861+ "version" : "1.40.0" ,
862+ "type" : "library" ,
863+ "licenses" : [ "MIT" ] ,
864+ } ,
865+ ] ,
866+ } ) ;
867+
868+ serde_json:: to_string_pretty ( & sbom) . unwrap_or_else ( |_| "{}" . to_string ( ) )
869+ }
870+
871+ fn render_scan_results_stub ( vulnerabilities : & [ SimulatedVulnerability ] ) -> String {
872+ let ( critical, high, medium, low) = Self :: count_vulnerabilities ( vulnerabilities) ;
873+ let total = critical + high + medium + low;
874+
875+ let findings: Vec < _ > = vulnerabilities
876+ . iter ( )
877+ . map ( |v| {
878+ json ! ( {
879+ "severity" : v. severity. to_string( ) ,
880+ "package" : v. package,
881+ "identifier" : v. identifier,
882+ "summary" : v. summary,
883+ } )
884+ } )
885+ . collect ( ) ;
886+
887+ let scan = json ! ( {
888+ "generated_at" : Utc :: now( ) . to_rfc3339( ) ,
889+ "summary" : {
890+ "critical" : critical,
891+ "high" : high,
892+ "medium" : medium,
893+ "low" : low,
894+ "total" : total,
895+ } ,
896+ "findings" : findings,
897+ } ) ;
898+
899+ serde_json:: to_string_pretty ( & scan) . unwrap_or_else ( |_| "{}" . to_string ( ) )
900+ }
901+
902+ fn render_report_stub ( format : & str , vulnerabilities : & [ SimulatedVulnerability ] ) -> String {
903+ let ( critical, high, medium, low) = Self :: count_vulnerabilities ( vulnerabilities) ;
904+ let total = critical + high + medium + low;
905+ let generated_at = Utc :: now ( ) . to_rfc3339 ( ) ;
906+
907+ match format. to_lowercase ( ) . as_str ( ) {
908+ "json" => {
909+ let report = json ! ( {
910+ "generated_at" : generated_at,
911+ "summary" : {
912+ "critical" : critical,
913+ "high" : high,
914+ "medium" : medium,
915+ "low" : low,
916+ "total" : total,
917+ } ,
918+ "compliance" : {
919+ "eo_14028" : true ,
920+ "nis2" : true ,
921+ "cra" : true ,
922+ } ,
923+ "notes" : "Simulated data – replace with real scan findings in production." ,
924+ } ) ;
925+
926+ serde_json:: to_string_pretty ( & report) . unwrap_or_else ( |_| "{}" . to_string ( ) )
927+ }
928+ "markdown" => {
929+ format ! (
930+ "# ShadowMap Security Report\n \n Generated: {}\n \n | Severity | Count |\n | --- | ---: |\n | Critical | {} |\n | High | {} |\n | Medium | {} |\n | Low | {} |\n | **Total** | **{}** |\n \n - EO 14028 compliant\n - NIS2 compliant\n - CRA compliant\n " ,
931+ generated_at, critical, high, medium, low, total
932+ )
933+ }
934+ "html" => {
935+ format ! (
936+ "<html><head><title>ShadowMap Report</title></head><body><h1>ShadowMap Security Report</h1><p>Generated: {}</p><table border=\" 1\" cellpadding=\" 6\" ><thead><tr><th>Severity</th><th>Count</th></tr></thead><tbody><tr><td>Critical</td><td>{}</td></tr><tr><td>High</td><td>{}</td></tr><tr><td>Medium</td><td>{}</td></tr><tr><td>Low</td><td>{}</td></tr><tr><td><strong>Total</strong></td><td><strong>{}</strong></td></tr></tbody></table><p>Compliance: EO 14028 ✅ | NIS2 ✅ | CRA ✅</p></body></html>" ,
937+ generated_at, critical, high, medium, low, total
938+ )
939+ }
940+ "pdf" => {
941+ format ! (
942+ "ShadowMap PDF report placeholder\n Generated: {}\n Total findings: {} (Critical: {}, High: {}, Medium: {}, Low: {})\n Compliance: EO 14028 ✓, NIS2 ✓, CRA ✓\n " ,
943+ generated_at, total, critical, high, medium, low
944+ )
945+ }
946+ _ => {
947+ format ! (
948+ "ShadowMap report generated at {}\n Total findings: {} (Critical: {}, High: {}, Medium: {}, Low: {})\n Compliance targets met: EO 14028, NIS2, CRA\n " ,
949+ generated_at, total, critical, high, medium, low
950+ )
951+ }
952+ }
953+ }
954+
955+ fn write_stub_file ( path : & Path , contents : & str ) -> Result < ( ) , BoxError > {
956+ if let Some ( parent) = path. parent ( ) {
957+ if !parent. as_os_str ( ) . is_empty ( ) {
958+ fs:: create_dir_all ( parent) . map_err ( |err| -> BoxError { Box :: new ( err) } ) ?;
959+ }
960+ }
961+
962+ fs:: write ( path, contents) . map_err ( |err| -> BoxError { Box :: new ( err) } ) ?;
963+
964+ Ok ( ( ) )
965+ }
812966}
0 commit comments