Skip to content

Commit 0b1c8d1

Browse files
Copilotmanoelhc
andauthored
Update scan command to display module names in output format and add wildcard filter matching (#9)
* Initial plan * Update scan output to include module names in format "<filename>": "module.<module_name>" Co-authored-by: manoelhc <185583+manoelhc@users.noreply.github.com> * Add test to verify scan returns module names Co-authored-by: manoelhc <185583+manoelhc@users.noreply.github.com> * Add wildcard matching support for scan filters Co-authored-by: manoelhc <185583+manoelhc@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: manoelhc <185583+manoelhc@users.noreply.github.com>
1 parent e9383b3 commit 0b1c8d1

File tree

3 files changed

+154
-24
lines changed

3 files changed

+154
-24
lines changed

src/lib.rs

Lines changed: 102 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

11201149
fn 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+
}

src/main.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ fn main() -> Result<()> {
6161
set_value(&query, &value, file.as_deref())?;
6262
}
6363
Commands::Scan { query, dir } => {
64-
let files = scan_files(&query, &dir)?;
65-
for file in files {
66-
println!("{}", file.display());
64+
let results = scan_files(&query, &dir)?;
65+
for (file, module_name) in results {
66+
println!("\"{}\": \"module.{}\"", file.display(), module_name);
6767
}
6868
}
6969
}

tests/scan_tests.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@ fn test_scan_with_path_filter() {
120120
assert_eq!(results.len(), 1);
121121
}
122122

123+
#[test]
124+
fn test_scan_with_wildcard_url_filter() {
125+
let files = vec![
126+
("main.tf", common::SIMPLE_MODULE_TF),
127+
("other.tf", common::MODULE_WITH_PATH_TF),
128+
];
129+
let temp_dir = common::create_test_dir_with_files(&files);
130+
131+
// Test wildcard matching with *
132+
let results = scan_files(
133+
"module.*.source[url==\"*terraform-aws-modules*vpc*\"]",
134+
temp_dir.path()
135+
).unwrap();
136+
assert_eq!(results.len(), 1); // Should match SIMPLE_MODULE_TF which has terraform-aws-modules/terraform-aws-vpc
137+
}
138+
139+
#[test]
140+
fn test_scan_with_wildcard_ref_filter() {
141+
let files = vec![
142+
("main.tf", common::SIMPLE_MODULE_TF),
143+
("other.tf", common::MODULE_WITH_PATH_TF),
144+
];
145+
let temp_dir = common::create_test_dir_with_files(&files);
146+
147+
// Test wildcard matching with ref
148+
let results = scan_files(
149+
"module.*.source[ref==\"v*.0.0\"]",
150+
temp_dir.path()
151+
).unwrap();
152+
assert_eq!(results.len(), 2); // Both have v*.0.0 pattern
153+
}
154+
123155
#[test]
124156
fn test_scan_nested_directories() {
125157
let files = vec![
@@ -219,7 +251,7 @@ fn test_scan_multiple_modules_in_one_file() {
219251
let temp_dir = common::create_test_dir_with_files(&files);
220252

221253
let results = scan_files("module.*", temp_dir.path()).unwrap();
222-
assert_eq!(results.len(), 1); // One file with multiple modules
254+
assert_eq!(results.len(), 2); // Two modules in one file
223255
}
224256

225257
#[test]
@@ -235,3 +267,19 @@ fn test_scan_specific_module_in_multi_module_file() {
235267
let results_vpc = scan_files("module.vpc", temp_dir.path()).unwrap();
236268
assert_eq!(results_vpc.len(), 1);
237269
}
270+
271+
#[test]
272+
fn test_scan_returns_module_names() {
273+
let files = vec![
274+
("main.tf", common::MULTIPLE_MODULES_TF),
275+
];
276+
let temp_dir = common::create_test_dir_with_files(&files);
277+
278+
let results = scan_files("module.*", temp_dir.path()).unwrap();
279+
assert_eq!(results.len(), 2);
280+
281+
// Verify that module names are returned
282+
let module_names: Vec<String> = results.iter().map(|(_, name)| name.clone()).collect();
283+
assert!(module_names.contains(&"vpc".to_string()));
284+
assert!(module_names.contains(&"eks".to_string()));
285+
}

0 commit comments

Comments
 (0)