@@ -477,6 +477,94 @@ impl AuditCoordinator {
477477 }
478478 }
479479
480+ /// Create a new audit coordinator with optional AI capabilities
481+ /// If api_key is None or empty, AI analysis will be disabled
482+ pub fn with_optional_ai ( api_key : Option < String > ) -> Self {
483+ match api_key {
484+ Some ( key) if !key. trim ( ) . is_empty ( ) => {
485+ println ! ( "🤖 AI analysis enabled with provided API key" ) ;
486+ Self :: with_ai ( key)
487+ }
488+ _ => {
489+ println ! ( "🔧 Running audit without AI analysis (no API key provided)" ) ;
490+ Self :: new ( )
491+ }
492+ }
493+ }
494+
495+ /// Detect if the current directory is a Rust workspace
496+ pub fn is_workspace ( & self ) -> bool {
497+ std:: path:: Path :: new ( "Cargo.toml" ) . exists ( ) &&
498+ std:: fs:: read_to_string ( "Cargo.toml" )
499+ . map ( |content| content. contains ( "[workspace]" ) )
500+ . unwrap_or ( false )
501+ }
502+
503+ /// Get all crate directories in a workspace
504+ pub fn get_workspace_crates ( & self ) -> Result < Vec < std:: path:: PathBuf > > {
505+ let mut crates = Vec :: new ( ) ;
506+
507+ if self . is_workspace ( ) {
508+ let cargo_toml = std:: fs:: read_to_string ( "Cargo.toml" ) ?;
509+
510+ // Simple parsing to find workspace members
511+ let mut in_workspace = false ;
512+ let mut in_members = false ;
513+
514+ for line in cargo_toml. lines ( ) {
515+ let line = line. trim ( ) ;
516+
517+ if line == "[workspace]" {
518+ in_workspace = true ;
519+ continue ;
520+ }
521+
522+ if in_workspace && line. starts_with ( "members" ) {
523+ in_members = true ;
524+ continue ;
525+ }
526+
527+ if in_members && line. starts_with ( '[' ) && !line. starts_with ( "members" ) {
528+ break ;
529+ }
530+
531+ if in_members && line. contains ( "\" " ) {
532+ if let Some ( member) = line. split ( '"' ) . nth ( 1 ) {
533+ let crate_path = std:: path:: PathBuf :: from ( member) ;
534+ if crate_path. join ( "Cargo.toml" ) . exists ( ) {
535+ crates. push ( crate_path) ;
536+ }
537+ }
538+ }
539+ }
540+ } else {
541+ // Single crate
542+ crates. push ( std:: path:: PathBuf :: from ( "." ) ) ;
543+ }
544+
545+ if crates. is_empty ( ) {
546+ crates. push ( std:: path:: PathBuf :: from ( "." ) ) ;
547+ }
548+
549+ Ok ( crates)
550+ }
551+
552+ /// Get AI client for testing purposes
553+ #[ cfg( test) ]
554+ pub fn ai_client ( & self ) -> & Option < OpenAIClient > {
555+ & self . ai_client
556+ }
557+
558+ /// Enhance existing findings with AI analysis (exposed for testing)
559+ #[ cfg( test) ]
560+ pub async fn enhance_findings_with_ai (
561+ & self ,
562+ ai_client : & OpenAIClient ,
563+ findings : Vec < AuditFinding > ,
564+ ) -> Vec < AuditFinding > {
565+ self . enhance_findings_with_ai_internal ( ai_client, findings) . await
566+ }
567+
480568 /// Run comprehensive security audit with enhanced modular architecture
481569 pub async fn run_security_audit ( & self ) -> Result < AuditReport > {
482570 println ! ( "🔍 Starting comprehensive security audit with enhanced modular system..." ) ;
@@ -485,6 +573,9 @@ impl AuditCoordinator {
485573 println ! ( "🤖 AI-powered analysis enabled" ) ;
486574 } else if self . ai_disabled {
487575 println ! ( "🤖 AI analysis disabled due to previous errors" ) ;
576+ } else {
577+ println ! ( "🔧 Running audit without AI analysis (no API key provided)" ) ;
578+ println ! ( "💡 Consider setting OPENAI_API_KEY environment variable for enhanced analysis" ) ;
488579 }
489580
490581 // Create diagnostic coordinator only when needed
@@ -515,7 +606,7 @@ impl AuditCoordinator {
515606 findings. extend ( ai_findings) ;
516607
517608 // Enhance existing findings with AI analysis (only critical/high)
518- findings = self . enhance_findings_with_ai ( ai_client, findings) . await ;
609+ findings = self . enhance_findings_with_ai_internal ( ai_client, findings) . await ;
519610 }
520611 }
521612
@@ -546,7 +637,7 @@ impl AuditCoordinator {
546637 }
547638
548639 /// Run audit using only the modular system (fallback when diagnostics fail)
549- async fn run_modular_audit_only ( & self ) -> Result < AuditReport > {
640+ pub async fn run_modular_audit_only ( & self ) -> Result < AuditReport > {
550641 println ! ( "🔧 Running modular security audit system..." ) ;
551642
552643 let mut findings = Vec :: new ( ) ;
@@ -591,38 +682,94 @@ impl AuditCoordinator {
591682
592683 println ! ( "🔍 Running modular security checks..." ) ;
593684
594- // Analyze Rust source files using the modular system
595- if let Ok ( entries) = std:: fs:: read_dir ( "src" ) {
596- for entry in entries. flatten ( ) {
597- if let Some ( ext) = entry. path ( ) . extension ( ) {
598- if ext == "rs" {
599- if let Ok ( content) = std:: fs:: read_to_string ( & entry. path ( ) ) {
600- let file_path = entry. path ( ) . display ( ) . to_string ( ) ;
601-
602- match self . modular_coordinator . audit_file ( & content, & file_path) {
603- Ok ( findings) => {
604- println ! ( " 📄 Analyzed {} - {} findings" , file_path, findings. len( ) ) ;
605- all_findings. extend ( findings) ;
606- }
607- Err ( e) => {
608- log:: warn!( "Failed to analyze file {}: {}" , file_path, e) ;
609- // Continue with other files even if one fails
610- }
611- }
612- }
613- }
614- }
685+ // Get all crates in the workspace
686+ let crates = self . get_workspace_crates ( ) ?;
687+
688+ if crates. len ( ) > 1 {
689+ println ! ( "📦 Detected workspace with {} crates" , crates. len( ) ) ;
690+ }
691+
692+ for crate_path in & crates {
693+ let src_dir = crate_path. join ( "src" ) ;
694+ let crate_name = crate_path. file_name ( )
695+ . and_then ( |n| n. to_str ( ) )
696+ . unwrap_or ( "root" ) ;
697+
698+ if src_dir. exists ( ) {
699+ println ! ( "🔍 Scanning crate: {}" , crate_name) ;
700+
701+ // Recursively scan all Rust files in the crate
702+ self . scan_rust_files_recursive ( & src_dir, & mut all_findings, crate_name) . await ?;
615703 }
616704 }
617705
618- // Add configuration and dependency checks
619- all_findings. extend ( self . check_dependency_security_enhanced ( ) ?) ;
620- all_findings. extend ( self . check_configuration_security_enhanced ( ) ?) ;
706+ // Add configuration and dependency checks for each crate
707+ for crate_path in & crates {
708+ let current_dir = std:: env:: current_dir ( ) ?;
709+ if let Err ( e) = std:: env:: set_current_dir ( crate_path) {
710+ log:: warn!( "Failed to change directory to {}: {}" , crate_path. display( ) , e) ;
711+ continue ;
712+ }
713+
714+ all_findings. extend ( self . check_dependency_security_enhanced ( ) ?) ;
715+ all_findings. extend ( self . check_configuration_security_enhanced ( ) ?) ;
716+
717+ // Restore original directory
718+ if let Err ( e) = std:: env:: set_current_dir ( & current_dir) {
719+ log:: warn!( "Failed to restore directory: {}" , e) ;
720+ }
721+ }
621722
622723 println ! ( "✅ Modular security checks completed - {} findings" , all_findings. len( ) ) ;
623724 Ok ( all_findings)
624725 }
625726
727+ /// Recursively scan Rust files in a directory
728+ fn scan_rust_files_recursive < ' a > (
729+ & ' a self ,
730+ dir : & ' a std:: path:: Path ,
731+ findings : & ' a mut Vec < AuditFinding > ,
732+ crate_name : & ' a str
733+ ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < ( ) > > + ' a > > {
734+ Box :: pin ( async move {
735+ if let Ok ( entries) = std:: fs:: read_dir ( dir) {
736+ for entry in entries. flatten ( ) {
737+ let path = entry. path ( ) ;
738+
739+ if path. is_dir ( ) {
740+ // Recursively scan subdirectories
741+ self . scan_rust_files_recursive ( & path, findings, crate_name) . await ?;
742+ } else if let Some ( ext) = path. extension ( ) {
743+ if ext == "rs" {
744+ if let Ok ( content) = std:: fs:: read_to_string ( & path) {
745+ let file_path = format ! ( "{}/{}" , crate_name, path. display( ) ) ;
746+
747+ match self . modular_coordinator . audit_file ( & content, & file_path) {
748+ Ok ( mut file_findings) => {
749+ // Tag findings with the crate name
750+ for finding in & mut file_findings {
751+ finding. code_location = Some ( format ! ( "{}:{}" , crate_name,
752+ finding. code_location. as_ref( ) . unwrap_or( & "unknown" . to_string( ) ) ) ) ;
753+ }
754+
755+ println ! ( " 📄 Analyzed {} - {} findings" , file_path, file_findings. len( ) ) ;
756+ findings. extend ( file_findings) ;
757+ }
758+ Err ( e) => {
759+ log:: warn!( "Failed to analyze file {}: {}" , file_path, e) ;
760+ // Continue with other files even if one fails
761+ }
762+ }
763+ }
764+ }
765+ }
766+ }
767+ }
768+
769+ Ok ( ( ) )
770+ } )
771+ }
772+
626773 /// Create a fallback audit report when diagnostics fail
627774 async fn create_fallback_audit_report ( & self ) -> Result < AuditReport > {
628775 let mut findings = Vec :: new ( ) ;
@@ -819,7 +966,7 @@ impl AuditCoordinator {
819966 }
820967
821968 /// Enhance existing findings with AI analysis with improved error handling
822- async fn enhance_findings_with_ai (
969+ async fn enhance_findings_with_ai_internal (
823970 & self ,
824971 ai_client : & OpenAIClient ,
825972 findings : Vec < AuditFinding > ,
0 commit comments