@@ -520,6 +520,85 @@ pub struct AuditFinding {
520520 pub references : Vec < String > ,
521521}
522522
523+ /// Location where a finding was detected
524+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
525+ pub struct FindingLocation {
526+ pub file_path : String ,
527+ pub line_number : usize ,
528+ pub code_context : Option < String > ,
529+ }
530+
531+ /// Grouped audit findings of the same type in the same file
532+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
533+ pub struct GroupedFinding {
534+ pub id : String ,
535+ pub title : String ,
536+ pub description : String ,
537+ pub severity : AuditSeverity ,
538+ pub category : String ,
539+ pub cwe_id : Option < String > ,
540+ pub cvss_score : Option < f32 > ,
541+ pub impact : String ,
542+ pub recommendation : String ,
543+ pub references : Vec < String > ,
544+ pub locations : Vec < FindingLocation > ,
545+ pub instance_count : usize ,
546+ pub ai_analysis : Option < AIAnalysis > ,
547+ }
548+
549+ impl GroupedFinding {
550+ /// Create a new grouped finding from a single finding
551+ pub fn from_finding ( finding : & AuditFinding ) -> Self {
552+ let location = if let Some ( file_path) = & finding. code_location {
553+ vec ! [ FindingLocation {
554+ file_path: file_path. clone( ) ,
555+ line_number: 1 , // Default for now, will be improved with proper line detection
556+ code_context: None ,
557+ } ]
558+ } else {
559+ vec ! [ ]
560+ } ;
561+
562+ Self {
563+ id : finding. id . clone ( ) ,
564+ title : finding. title . clone ( ) ,
565+ description : finding. description . clone ( ) ,
566+ severity : finding. severity . clone ( ) ,
567+ category : finding. category . clone ( ) ,
568+ cwe_id : finding. cwe_id . clone ( ) ,
569+ cvss_score : finding. cvss_score ,
570+ impact : finding. impact . clone ( ) ,
571+ recommendation : finding. recommendation . clone ( ) ,
572+ references : finding. references . clone ( ) ,
573+ locations : location,
574+ instance_count : 1 ,
575+ ai_analysis : None ,
576+ }
577+ }
578+
579+ /// Add a finding instance to this group
580+ pub fn add_instance ( & mut self , finding : & AuditFinding ) {
581+ if let Some ( file_path) = & finding. code_location {
582+ self . locations . push ( FindingLocation {
583+ file_path : file_path. clone ( ) ,
584+ line_number : 1 , // Will be improved with proper line detection
585+ code_context : None ,
586+ } ) ;
587+ }
588+ self . instance_count += 1 ;
589+ }
590+
591+ /// Get a unique key for grouping findings
592+ pub fn grouping_key ( finding : & AuditFinding ) -> String {
593+ format ! ( "{}::{}" , finding. title, finding. category)
594+ }
595+
596+ /// Check if a finding belongs to this group
597+ pub fn matches_finding ( & self , finding : & AuditFinding ) -> bool {
598+ self . title == finding. title && self . category == finding. category
599+ }
600+ }
601+
523602/// Audit summary statistics
524603#[ derive( Debug , Clone , Serialize , Deserialize ) ]
525604pub struct AuditSummary {
@@ -789,6 +868,164 @@ impl AuditCoordinator {
789868 )
790869 }
791870
871+ /// Group findings by type and file for better reporting
872+ fn group_findings ( & self , findings : Vec < AuditFinding > ) -> Vec < GroupedFinding > {
873+ let mut groups: HashMap < String , GroupedFinding > = HashMap :: new ( ) ;
874+
875+ for finding in findings {
876+ let grouping_key = GroupedFinding :: grouping_key ( & finding) ;
877+
878+ match groups. get_mut ( & grouping_key) {
879+ Some ( group) => {
880+ group. add_instance ( & finding) ;
881+ }
882+ None => {
883+ let group = GroupedFinding :: from_finding ( & finding) ;
884+ groups. insert ( grouping_key, group) ;
885+ }
886+ }
887+ }
888+
889+ // Convert to vector and sort by severity and instance count
890+ let mut grouped_findings: Vec < GroupedFinding > = groups. into_values ( ) . collect ( ) ;
891+ grouped_findings. sort_by ( |a, b| {
892+ // Sort by severity first (Critical > High > Medium > Low > Info)
893+ let severity_order = |severity : & AuditSeverity | match severity {
894+ AuditSeverity :: Critical => 0 ,
895+ AuditSeverity :: High => 1 ,
896+ AuditSeverity :: Medium => 2 ,
897+ AuditSeverity :: Low => 3 ,
898+ AuditSeverity :: Info => 4 ,
899+ } ;
900+
901+ let severity_cmp = severity_order ( & a. severity ) . cmp ( & severity_order ( & b. severity ) ) ;
902+ if severity_cmp != std:: cmp:: Ordering :: Equal {
903+ return severity_cmp;
904+ }
905+
906+ // Then by instance count (descending)
907+ b. instance_count . cmp ( & a. instance_count )
908+ } ) ;
909+
910+ grouped_findings
911+ }
912+
913+ /// Convert grouped findings back to individual findings for compatibility
914+ fn ungrouped_findings_from_groups ( & self , groups : & [ GroupedFinding ] ) -> Vec < AuditFinding > {
915+ let mut findings = Vec :: new ( ) ;
916+
917+ for group in groups {
918+ for ( index, location) in group. locations . iter ( ) . enumerate ( ) {
919+ let finding_id = if index == 0 {
920+ group. id . clone ( )
921+ } else {
922+ format ! ( "{}-{}" , group. id, index + 1 )
923+ } ;
924+
925+ findings. push ( AuditFinding {
926+ id : finding_id,
927+ title : group. title . clone ( ) ,
928+ description : if group. instance_count > 1 {
929+ format ! ( "{} (Instance {} of {})" , group. description, index + 1 , group. instance_count)
930+ } else {
931+ group. description . clone ( )
932+ } ,
933+ severity : group. severity . clone ( ) ,
934+ category : group. category . clone ( ) ,
935+ cwe_id : group. cwe_id . clone ( ) ,
936+ cvss_score : group. cvss_score ,
937+ impact : group. impact . clone ( ) ,
938+ recommendation : group. recommendation . clone ( ) ,
939+ code_location : Some ( format ! ( "{}:{}" , location. file_path, location. line_number) ) ,
940+ references : group. references . clone ( ) ,
941+ } ) ;
942+ }
943+ }
944+
945+ findings
946+ }
947+
948+ /// Enhance grouped findings with AI analysis (more efficient than per-finding analysis)
949+ async fn enhance_grouped_findings_with_ai ( & self , mut groups : Vec < GroupedFinding > ) -> Vec < GroupedFinding > {
950+ let mut ai_failures = 0 ;
951+ let mut ai_successes = 0 ;
952+
953+ for group in & mut groups {
954+ // Only analyze Critical and High severity findings
955+ if group. severity == AuditSeverity :: Critical || group. severity == AuditSeverity :: High {
956+ // Check circuit breaker before each AI call
957+ if self . ai_circuit_breaker . is_open ( ) {
958+ log:: warn!(
959+ "AI circuit breaker is open, skipping AI enhancement for group {}" ,
960+ group. id
961+ ) ;
962+ continue ;
963+ }
964+
965+ // Create a representative finding for AI analysis
966+ let representative_finding = AuditFinding {
967+ id : group. id . clone ( ) ,
968+ title : group. title . clone ( ) ,
969+ description : format ! (
970+ "{} (Found in {} locations with {} instances)" ,
971+ group. description, group. locations. len( ) , group. instance_count
972+ ) ,
973+ severity : group. severity . clone ( ) ,
974+ category : group. category . clone ( ) ,
975+ cwe_id : group. cwe_id . clone ( ) ,
976+ cvss_score : group. cvss_score ,
977+ impact : group. impact . clone ( ) ,
978+ recommendation : group. recommendation . clone ( ) ,
979+ code_location : group. locations . first ( ) . map ( |loc| loc. file_path . clone ( ) ) ,
980+ references : group. references . clone ( ) ,
981+ } ;
982+
983+ match self . query_ai_for_finding ( & representative_finding) . await {
984+ Ok ( ai_analysis) => {
985+ println ! ( "🤖 AI analysis completed for group: {} ({} instances)" , group. title, group. instance_count) ;
986+ group. ai_analysis = Some ( ai_analysis) ;
987+ ai_successes += 1 ;
988+
989+ // Record success with circuit breaker
990+ self . ai_circuit_breaker . record_success ( ) ;
991+ }
992+ Err ( e) => {
993+ // Record failure with circuit breaker
994+ self . ai_circuit_breaker . record_failure ( ) ;
995+
996+ // Handle error with enhanced error handler
997+ if let Some ( fallback_msg) = EnhancedAIErrorHandler :: handle_ai_error (
998+ & e,
999+ & format ! ( "grouped finding {}" , group. id) ,
1000+ ) {
1001+ log:: warn!(
1002+ "AI enhancement failed for group {}: {}" ,
1003+ group. id,
1004+ fallback_msg
1005+ ) ;
1006+ }
1007+
1008+ ai_failures += 1 ;
1009+ if EnhancedAIErrorHandler :: should_disable_ai ( & e) {
1010+ log:: error!( "Disabling AI analysis due to: {}" , e) ;
1011+ break ;
1012+ }
1013+ }
1014+ }
1015+ }
1016+ }
1017+
1018+ if ai_failures > 0 || ai_successes > 0 {
1019+ log:: info!(
1020+ "AI enhancement completed for grouped findings: {} successes, {} failures" ,
1021+ ai_successes,
1022+ ai_failures
1023+ ) ;
1024+ }
1025+
1026+ groups
1027+ }
1028+
7921029 /// Run comprehensive security audit with enhanced modular architecture
7931030 pub async fn run_security_audit ( & self ) -> Result < AuditReport > {
7941031 println ! ( "🔍 Starting comprehensive security audit with enhanced modular system..." ) ;
@@ -828,20 +1065,36 @@ impl AuditCoordinator {
8281065 // Run enhanced modular security checks
8291066 findings. extend ( self . run_modular_security_checks ( ) . await ?) ;
8301067
831- // If AI is enabled and not disabled, perform AI-enhanced analysis
1068+ // Group findings for better reporting and optimized AI analysis
1069+ println ! ( "📊 Grouping findings for enhanced reporting..." ) ;
1070+ let mut grouped_findings = self . group_findings ( findings) ;
1071+
1072+ // Print grouping summary
1073+ let total_instances: usize = grouped_findings. iter ( ) . map ( |g| g. instance_count ) . sum ( ) ;
1074+ let total_groups = grouped_findings. len ( ) ;
1075+ println ! ( "📋 Grouped {} findings into {} groups" , total_instances, total_groups) ;
1076+
1077+ // If AI is enabled and not disabled, perform AI-enhanced analysis on groups
8321078 if self . should_use_ai ( ) {
833- println ! ( "🤖 Running AI-powered analysis..." ) ;
1079+ println ! ( "🤖 Running optimized AI-powered analysis on grouped findings ..." ) ;
8341080
835- // For OpenAI client, perform code analysis
1081+ // For OpenAI client, perform code analysis (if needed)
8361082 if let Some ( ref ai_client) = self . ai_client {
8371083 let ai_findings = self . perform_ai_code_analysis ( ai_client) . await ;
838- findings. extend ( ai_findings) ;
1084+ if !ai_findings. is_empty ( ) {
1085+ println ! ( "🔍 Integrating {} AI-discovered findings..." , ai_findings. len( ) ) ;
1086+ let ai_groups = self . group_findings ( ai_findings) ;
1087+ grouped_findings. extend ( ai_groups) ;
1088+ }
8391089 }
8401090
841- // Enhance existing findings with AI analysis (only critical/high )
842- findings = self . enhance_findings_with_ai_internal ( findings ) . await ;
1091+ // Enhance grouped findings with AI analysis (much more efficient )
1092+ grouped_findings = self . enhance_grouped_findings_with_ai ( grouped_findings ) . await ;
8431093 }
8441094
1095+ // Convert back to individual findings for report compatibility
1096+ let findings = self . ungrouped_findings_from_groups ( & grouped_findings) ;
1097+
8451098 // Generate system information
8461099 let system_info = self . collect_system_info ( ) . await ?;
8471100
@@ -892,6 +1145,16 @@ impl AuditCoordinator {
8921145 references : vec ! [ "https://cwe.mitre.org/data/definitions/754.html" . to_string( ) ] ,
8931146 } ) ;
8941147
1148+ // Group findings for better reporting
1149+ println ! ( "📊 Grouping findings for enhanced reporting..." ) ;
1150+ let grouped_findings = self . group_findings ( findings) ;
1151+ let total_instances: usize = grouped_findings. iter ( ) . map ( |g| g. instance_count ) . sum ( ) ;
1152+ let total_groups = grouped_findings. len ( ) ;
1153+ println ! ( "📋 Grouped {} findings into {} groups" , total_instances, total_groups) ;
1154+
1155+ // Convert back to individual findings for report compatibility
1156+ let findings = self . ungrouped_findings_from_groups ( & grouped_findings) ;
1157+
8951158 let system_info = self . collect_system_info ( ) . await ?;
8961159 let summary = self . calculate_audit_summary ( & findings) ;
8971160 let recommendations = self . generate_security_recommendations ( & findings) ;
0 commit comments