@@ -1020,57 +1020,71 @@ pub fn find_all_tf_files(dir: &std::path::Path) -> Result<Vec<PathBuf>> {
10201020 Ok ( tf_files)
10211021}
10221022
1023- pub fn scan_files ( query : & str , dir : & std:: path:: Path ) -> Result < Vec < PathBuf > > {
1023+ pub fn scan_files ( query : & str , dir : & std:: path:: Path ) -> Result < Vec < ( PathBuf , String ) > > {
10241024 let scan_query = parse_scan_query ( query) ?;
10251025 let tf_files = find_all_tf_files ( dir) ?;
10261026
1027- let mut matching_files = Vec :: new ( ) ;
1027+ let mut results = Vec :: new ( ) ;
10281028
10291029 for file_path in tf_files {
1030- if matches_query ( & file_path, & scan_query) ? {
1031- matching_files. push ( file_path) ;
1030+ let module_names = find_matching_modules ( & file_path, & scan_query) ?;
1031+ for module_name in module_names {
1032+ results. push ( ( file_path. clone ( ) , module_name) ) ;
10321033 }
10331034 }
10341035
1035- Ok ( matching_files )
1036+ Ok ( results )
10361037}
10371038
1038- fn matches_query ( file_path : & std:: path:: Path , scan_query : & ScanQuery ) -> Result < bool > {
1039+ fn find_matching_modules ( file_path : & std:: path:: Path , scan_query : & ScanQuery ) -> Result < Vec < String > > {
10391040 let content = fs:: read_to_string ( file_path)
10401041 . with_context ( || format ! ( "Failed to read file: {:?}" , file_path) ) ?;
10411042
10421043 let body: Body = content
10431044 . parse ( )
10441045 . with_context ( || format ! ( "Failed to parse HCL: {:?}" , file_path) ) ?;
10451046
1047+ let mut matching_modules = Vec :: new ( ) ;
1048+
10461049 // Look for blocks matching the query
10471050 for structure in body. iter ( ) {
10481051 if let Some ( block) = structure. as_block ( ) {
10491052 if block. ident . as_str ( ) != scan_query. block_type {
10501053 continue ;
10511054 }
10521055
1056+ // Get the block label (module name for module blocks)
1057+ let labels: Vec < String > = block
1058+ . labels
1059+ . iter ( )
1060+ . map ( |l| l. as_str ( ) . to_string ( ) )
1061+ . collect ( ) ;
1062+
1063+ let block_label = labels. first ( ) . map ( |s| s. as_str ( ) ) ;
1064+
10531065 // Check block label if specified
10541066 if let Some ( ref expected_label) = scan_query. block_label {
1055- let labels: Vec < String > = block
1056- . labels
1057- . iter ( )
1058- . map ( |l| l. as_str ( ) . to_string ( ) )
1059- . collect ( ) ;
1060-
1061- if labels. first ( ) . map ( |s| s. as_str ( ) ) != Some ( expected_label. as_str ( ) ) {
1067+ if block_label != Some ( expected_label. as_str ( ) ) {
10621068 continue ;
10631069 }
10641070 }
10651071
10661072 // If no nested blocks or attribute specified, we found a match
10671073 if scan_query. nested_blocks . is_empty ( ) && scan_query. attribute . is_none ( ) {
1068- return Ok ( true ) ;
1074+ // For blocks with labels (like modules), use the label
1075+ // For blocks without labels (like terraform), use the block type
1076+ if let Some ( label) = block_label {
1077+ matching_modules. push ( label. to_string ( ) ) ;
1078+ } else {
1079+ matching_modules. push ( scan_query. block_type . clone ( ) ) ;
1080+ }
1081+ continue ;
10691082 }
10701083
10711084 // Navigate through nested blocks
10721085 let mut current_body = & block. body ;
10731086
1087+ let mut nested_matched = true ;
10741088 for nested_name in & scan_query. nested_blocks {
10751089 let mut found_this_level = false ;
10761090
@@ -1085,11 +1099,15 @@ fn matches_query(file_path: &std::path::Path, scan_query: &ScanQuery) -> Result<
10851099 }
10861100
10871101 if !found_this_level {
1088- // Couldn't find nested block, so this file doesn't match
1089- return Ok ( false ) ;
1102+ nested_matched = false ;
1103+ break ;
10901104 }
10911105 }
10921106
1107+ if !nested_matched {
1108+ continue ;
1109+ }
1110+
10931111 // Check attribute if specified
10941112 if let Some ( ref attr_name) = scan_query. attribute {
10951113 for item in current_body. iter ( ) {
@@ -1103,27 +1121,91 @@ fn matches_query(file_path: &std::path::Path, scan_query: &ScanQuery) -> Result<
11031121 }
11041122 }
11051123
1106- return Ok ( true ) ;
1124+ // For blocks with labels, use the label
1125+ // For blocks without labels, use the block type
1126+ if let Some ( label) = block_label {
1127+ matching_modules. push ( label. to_string ( ) ) ;
1128+ } else {
1129+ matching_modules. push ( scan_query. block_type . clone ( ) ) ;
1130+ }
1131+ break ;
11071132 }
11081133 }
11091134 }
11101135 } else {
11111136 // No specific attribute required, nested blocks matched
1112- return Ok ( true ) ;
1137+ if let Some ( label) = block_label {
1138+ matching_modules. push ( label. to_string ( ) ) ;
1139+ } else {
1140+ matching_modules. push ( scan_query. block_type . clone ( ) ) ;
1141+ }
11131142 }
11141143 }
11151144 }
11161145
1117- Ok ( false )
1146+ Ok ( matching_modules )
11181147}
11191148
11201149fn matches_filter ( value_str : & str , filter : & AttributeFilter ) -> Result < bool > {
11211150 // Extract the value based on the filter attribute (url, ref, path, etc.)
11221151 let extracted = extract_param_from_source ( value_str, & filter. attribute ) ?;
11231152
11241153 if let Some ( extracted_value) = extracted {
1125- Ok ( extracted_value == filter. value )
1154+ Ok ( wildcard_match ( & filter. value , & extracted_value ) )
11261155 } else {
11271156 Ok ( false )
11281157 }
11291158}
1159+
1160+ fn wildcard_match ( pattern : & str , text : & str ) -> bool {
1161+ // Simple wildcard matching with * as wildcard
1162+ // If no wildcards, do exact match
1163+ if !pattern. contains ( '*' ) {
1164+ return pattern == text;
1165+ }
1166+
1167+ let parts: Vec < & str > = pattern. split ( '*' ) . collect ( ) ;
1168+
1169+ // Handle edge cases
1170+ if parts. is_empty ( ) {
1171+ return true ;
1172+ }
1173+
1174+ let mut text_pos = 0 ;
1175+
1176+ for ( i, part) in parts. iter ( ) . enumerate ( ) {
1177+ if part. is_empty ( ) {
1178+ continue ;
1179+ }
1180+
1181+ // First part must match at the beginning (unless pattern starts with *)
1182+ if i == 0 && !pattern. starts_with ( '*' ) {
1183+ if !text[ text_pos..] . starts_with ( part) {
1184+ return false ;
1185+ }
1186+ text_pos += part. len ( ) ;
1187+ }
1188+ // Last part must match at the end (unless pattern ends with *)
1189+ else if i == parts. len ( ) - 1 && !pattern. ends_with ( '*' ) {
1190+ if !text[ text_pos..] . ends_with ( part) {
1191+ return false ;
1192+ }
1193+ // Move position to the end
1194+ if let Some ( pos) = text[ text_pos..] . rfind ( part) {
1195+ text_pos += pos + part. len ( ) ;
1196+ } else {
1197+ return false ;
1198+ }
1199+ }
1200+ // Middle parts can be anywhere after current position
1201+ else {
1202+ if let Some ( pos) = text[ text_pos..] . find ( part) {
1203+ text_pos += pos + part. len ( ) ;
1204+ } else {
1205+ return false ;
1206+ }
1207+ }
1208+ }
1209+
1210+ true
1211+ }
0 commit comments