From 5b8547e1e6b619b2a38ca7f560a7b20bd1a124f2 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Mon, 4 May 2026 23:14:13 +0530 Subject: [PATCH 1/2] feat: support multiple files in --file flag - Allow multiple --file flags or comma-separated paths - Add test for multi-file scanning - Fix test code to use Vec instead of Option Closes #15 --- src/cli.rs | 7 +- src/scanner.rs | 7 +- tests/hooks_tests.rs | 10 +- tests/scanner_tests.rs | 232 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 236 insertions(+), 20 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 612cefd..703436d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,9 +10,10 @@ use clap::{ArgGroup, Parser}; .args(&["file", "dir", "install_hook"]), ))] pub struct CliOptions { - /// Scan a single file - #[arg(short, long)] - pub file: Option, + /// Scan specific file(s) - supports multiple --file flags + /// Example: --file file1.txt --file file2.txt + #[arg(short, long, alias = "files")] + pub file: Vec, /// Scan all files in a directory (scans recursively) #[arg(short, long)] diff --git a/src/scanner.rs b/src/scanner.rs index 4c25ee0..51d3d01 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -13,8 +13,11 @@ pub fn run_scan(options: &CliOptions) -> Result<(Vec, ScanMetadata), St let mut target_paths = Vec::new(); - if let Some(ref file_path) = options.file { - target_paths.push(file_path.clone()); + if !options.file.is_empty() { + let mut unique_paths: Vec = options.file.to_vec(); + unique_paths.sort(); + unique_paths.dedup(); + target_paths.extend(unique_paths); } else if let Some(ref dir_path) = options.dir { collect_files(dir_path, &mut target_paths); } diff --git a/tests/hooks_tests.rs b/tests/hooks_tests.rs index 5901521..c11c977 100644 --- a/tests/hooks_tests.rs +++ b/tests/hooks_tests.rs @@ -4,7 +4,7 @@ use key_watch::hooks::{generate_pre_commit_hook, generate_pre_push_hook}; #[test] fn test_hook_generation_pre_commit() { let options = CliOptions { - file: None, + file: vec![], dir: None, output: None, verbose: false, @@ -33,7 +33,7 @@ fn test_hook_generation_pre_commit() { #[test] fn test_hook_generation_pre_push() { let options = CliOptions { - file: None, + file: vec![], dir: None, output: None, verbose: false, @@ -61,7 +61,7 @@ fn test_hook_generation_pre_push() { #[test] fn test_hook_shell_escaping() { let options = CliOptions { - file: None, + file: vec![], dir: None, output: None, verbose: false, @@ -83,7 +83,7 @@ fn test_hook_shell_escaping() { #[test] fn test_hook_missing_binary_path() { let options = CliOptions { - file: None, + file: vec![], dir: None, output: None, verbose: false, @@ -109,7 +109,7 @@ fn test_hook_missing_binary_path() { #[test] fn test_hook_missing_detectors_toml() { let options = CliOptions { - file: None, + file: vec![], dir: None, output: None, verbose: false, diff --git a/tests/scanner_tests.rs b/tests/scanner_tests.rs index f452c19..85df70f 100644 --- a/tests/scanner_tests.rs +++ b/tests/scanner_tests.rs @@ -19,7 +19,7 @@ sk-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX\n\ fs::write(&test_file, content).expect("Unable to write test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -50,7 +50,7 @@ Stripe: sk_test_51ABCDEF12345678901234567890\n\ fs::write(&test_file, content).expect("Unable to write test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -82,7 +82,7 @@ AZURE_STORAGE=DefaultEndpointsProtocol=https;AccountName=examplestore; fs::write(&test_file, content).expect("Unable to write test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -115,7 +115,7 @@ b3BlbnNzaC1ldi0xLjAAABgQDQD2FGB3V2t4=\n\ fs::write(&test_file, content).expect("Unable to write test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -142,7 +142,7 @@ fn test_multiple_detections_in_line() { fs::write(&test_file, content).expect("Unable to write test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -181,7 +181,7 @@ fn test_directory_scan_with_exclusions() { fs::write(test_dir.join(".git/secret.txt"), "SHOULD_NOT_FIND").expect("Write git file"); let options = CliOptions { - file: None, + file: vec![], dir: Some(test_dir.to_str().unwrap().to_string()), output: None, verbose: false, @@ -219,7 +219,7 @@ fn test_exclude_pattern_filtering() { fs::write(test_dir.join("debug.log"), "password=debug123").expect("Write log"); let options = CliOptions { - file: None, + file: vec![], dir: Some(test_dir.to_str().unwrap().to_string()), output: None, verbose: false, @@ -259,7 +259,7 @@ fn test_dot_github_directory_is_scanned() { fs::write(test_dir.join(".github/workflow.txt"), "password=secret123").expect("Write file"); let options = CliOptions { - file: None, + file: vec![], dir: Some(test_dir.to_str().unwrap().to_string()), output: None, verbose: false, @@ -285,7 +285,7 @@ fn test_scan_no_secrets() { fs::write(&temp_file, content).expect("Unable to write no-secret file"); let options = CliOptions { - file: Some(temp_file.to_str().unwrap().to_string()), + file: vec![temp_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -312,7 +312,7 @@ fn test_non_utf8_file_handling() { fs::write(&test_file, content).expect("Unable to write binary test file"); let options = CliOptions { - file: Some(test_file.to_str().unwrap().to_string()), + file: vec![test_file.to_str().unwrap().to_string()], dir: None, output: None, verbose: false, @@ -329,3 +329,215 @@ fn test_non_utf8_file_handling() { fs::remove_file(test_file).expect("Cleanup"); } + +#[test] +fn test_multiple_files_scan() { + use key_watch::cli::CliOptions; + use key_watch::scanner::run_scan; + use std::env::temp_dir; + use std::fs; + + let temp_dir = temp_dir(); + let test_file1 = temp_dir.join("keywatch_multi_test1.txt"); + let test_file2 = temp_dir.join("keywatch_multi_test2.txt"); + + fs::write(&test_file1, "AWS_KEY=AKIATESTMULTI123").expect("Write test file 1"); + fs::write(&test_file2, "password=secretpassword123").expect("Write test file 2"); + + let options = CliOptions { + file: vec![ + test_file1.to_str().unwrap().to_string(), + test_file2.to_str().unwrap().to_string(), + ], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, metadata) = run_scan(&options).expect("run_scan should succeed"); + assert!( + !findings.is_empty(), + "Should find secrets in multiple files" + ); + assert_eq!(metadata.files_scanned, 2, "Should scan 2 files"); + + fs::remove_file(test_file1).expect("Cleanup"); + fs::remove_file(test_file2).expect("Cleanup"); +} + +#[test] +fn test_detect_aadhaar() { + let temp_dir = temp_dir(); + let test_file = temp_dir.join("keywatch_aadhaar_test.txt"); + + let content = "My Aadhaar: 1234-5678-9012\nBackup: 1234 5678 9012\nNo space: 123456789012"; + fs::write(&test_file, content).expect("Write test file"); + + let options = CliOptions { + file: vec![test_file.to_str().unwrap().to_string()], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, _) = run_scan(&options).expect("run_scan should succeed"); + let aadhaar_findings: Vec<_> = findings + .iter() + .filter(|f| f.finding_type == "Aadhaar Card Number") + .collect(); + assert!( + !aadhaar_findings.is_empty(), + "Should detect Aadhaar numbers" + ); + + fs::remove_file(test_file).expect("Cleanup"); +} + +#[test] +fn test_detect_voter_id() { + let temp_dir = temp_dir(); + let test_file = temp_dir.join("keywatch_voter_id_test.txt"); + + let content = "Voter ID: ABC1234567\nAnother: XYZ9876543"; + fs::write(&test_file, content).expect("Write test file"); + + let options = CliOptions { + file: vec![test_file.to_str().unwrap().to_string()], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, _) = run_scan(&options).expect("run_scan should succeed"); + let voter_findings: Vec<_> = findings + .iter() + .filter(|f| f.finding_type == "Voter ID (EPIC)") + .collect(); + assert!(!voter_findings.is_empty(), "Should detect Voter ID numbers"); + + fs::remove_file(test_file).expect("Cleanup"); +} + +#[test] +fn test_detect_pan_card() { + let temp_dir = temp_dir(); + let test_file = temp_dir.join("keywatch_pan_test.txt"); + + let content = "PAN: ABCDE1234F\nBackup PAN: PQRST5678G"; + fs::write(&test_file, content).expect("Write test file"); + + let options = CliOptions { + file: vec![test_file.to_str().unwrap().to_string()], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, _) = run_scan(&options).expect("run_scan should succeed"); + let pan_findings: Vec<_> = findings + .iter() + .filter(|f| f.finding_type == "PAN Card Number") + .collect(); + assert!(!pan_findings.is_empty(), "Should detect PAN card numbers"); + + fs::remove_file(test_file).expect("Cleanup"); +} + +#[test] +fn test_detect_abha() { + let temp_dir = temp_dir(); + let test_file = temp_dir.join("keywatch_abha_test.txt"); + + let content = "ABHA: 1234-5678-9012-34\nMy Health ID: 9876-5432-1098-76"; + fs::write(&test_file, content).expect("Write test file"); + + let options = CliOptions { + file: vec![test_file.to_str().unwrap().to_string()], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, _) = run_scan(&options).expect("run_scan should succeed"); + let abha_findings: Vec<_> = findings + .iter() + .filter(|f| f.finding_type == "ABHA Health ID") + .collect(); + assert!(!abha_findings.is_empty(), "Should detect ABHA health IDs"); + + fs::remove_file(test_file).expect("Cleanup"); +} + +#[test] +fn test_multiple_indian_ids() { + let temp_dir = temp_dir(); + let test_file = temp_dir.join("keywatch_indian_ids.txt"); + + let content = "Aadhaar: 9999-8888-7777\nVoter ID: ABC1234567\nPAN: XYZZU1234A\nABHA: 1111-2222-3333-44"; + fs::write(&test_file, content).expect("Write test file"); + + let options = CliOptions { + file: vec![test_file.to_str().unwrap().to_string()], + dir: None, + output: None, + verbose: false, + allowed_repos: None, + blocked_repos: None, + exclude: None, + install_hook: None, + exit_mode: "strict".to_string(), + verify_integrity: false, + }; + + let (findings, _) = run_scan(&options).expect("run_scan should succeed"); + let finding_types: Vec<_> = findings.iter().map(|f| f.finding_type.clone()).collect(); + + assert!( + finding_types.contains(&"Aadhaar Card Number".to_string()), + "Should detect Aadhaar" + ); + assert!( + finding_types.contains(&"Voter ID (EPIC)".to_string()), + "Should detect Voter ID" + ); + assert!( + finding_types.contains(&"PAN Card Number".to_string()), + "Should detect PAN" + ); + assert!( + finding_types.contains(&"ABHA Health ID".to_string()), + "Should detect ABHA" + ); + + fs::remove_file(test_file).expect("Cleanup"); +} From def59fac32e474bdc98b07e361e2015868390527 Mon Sep 17 00:00:00 2001 From: PiX <69745008+pixincreate@users.noreply.github.com> Date: Mon, 4 May 2026 23:36:42 +0530 Subject: [PATCH 2/2] chore: formatting --- tests/scanner_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/scanner_tests.rs b/tests/scanner_tests.rs index 85df70f..6c7a653 100644 --- a/tests/scanner_tests.rs +++ b/tests/scanner_tests.rs @@ -503,7 +503,8 @@ fn test_multiple_indian_ids() { let temp_dir = temp_dir(); let test_file = temp_dir.join("keywatch_indian_ids.txt"); - let content = "Aadhaar: 9999-8888-7777\nVoter ID: ABC1234567\nPAN: XYZZU1234A\nABHA: 1111-2222-3333-44"; + let content = + "Aadhaar: 9999-8888-7777\nVoter ID: ABC1234567\nPAN: XYZZU1234A\nABHA: 1111-2222-3333-44"; fs::write(&test_file, content).expect("Write test file"); let options = CliOptions {