Skip to content

Commit c7b2f68

Browse files
Copilot0xrinegade
andcommitted
Implement line number detection and finding grouping - Phase 1 complete
Co-authored-by: 0xrinegade <[email protected]>
1 parent 2dfb455 commit c7b2f68

File tree

2 files changed

+399
-28
lines changed

2 files changed

+399
-28
lines changed

src/utils/audit.rs

Lines changed: 269 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
525604
pub 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

Comments
 (0)