Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
/// Scan specific file(s) - supports multiple --file flags
/// Example: --file file1.txt --file file2.txt
#[arg(short, long, alias = "files")]
pub file: Vec<String>,

/// Scan all files in a directory (scans recursively)
#[arg(short, long)]
Expand Down
7 changes: 5 additions & 2 deletions src/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ pub fn run_scan(options: &CliOptions) -> Result<(Vec<Finding>, 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<String> = 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);
}
Expand Down
10 changes: 5 additions & 5 deletions tests/hooks_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
233 changes: 223 additions & 10 deletions tests/scanner_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -329,3 +329,216 @@ 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(),
],
Comment thread
pixincreate marked this conversation as resolved.
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");
}
Loading