envvault/cli/commands/
search.rs1use crate::cli::output;
7use crate::cli::{load_keyfile, prompt_password_for_vault, vault_path, Cli};
8use crate::errors::Result;
9use crate::vault::VaultStore;
10
11pub fn execute(cli: &Cli, pattern: &str) -> Result<()> {
13 let path = vault_path(cli)?;
14 let keyfile = load_keyfile(cli)?;
15
16 let vault_id = path.to_string_lossy();
17 let password = prompt_password_for_vault(Some(&vault_id))?;
18 let store = VaultStore::open(&path, password.as_bytes(), keyfile.as_deref())?;
19
20 let secrets = store.list_secrets();
21 let matches: Vec<_> = secrets
22 .iter()
23 .filter(|s| glob_match(pattern, &s.name))
24 .collect();
25
26 if matches.is_empty() {
27 output::info(&format!("No secrets matching '{pattern}'"));
28 return Ok(());
29 }
30
31 output::info(&format!(
32 "{} secret(s) matching '{pattern}':",
33 matches.len()
34 ));
35 output::print_secrets_table(&matches.into_iter().cloned().collect::<Vec<_>>());
36
37 #[cfg(feature = "audit-log")]
38 crate::audit::log_read_audit(cli, "search", None, Some(&format!("pattern: {pattern}")));
39
40 Ok(())
41}
42
43pub fn glob_match(pattern: &str, text: &str) -> bool {
46 let pattern = pattern.to_ascii_lowercase();
47 let text = text.to_ascii_lowercase();
48 glob_match_inner(pattern.as_bytes(), text.as_bytes())
49}
50
51fn glob_match_inner(pattern: &[u8], text: &[u8]) -> bool {
52 let mut p = 0;
53 let mut t = 0;
54 let mut star_p = usize::MAX; let mut star_t = 0; while t < text.len() {
58 if p < pattern.len() && (pattern[p] == b'?' || pattern[p] == text[t]) {
59 p += 1;
60 t += 1;
61 } else if p < pattern.len() && pattern[p] == b'*' {
62 star_p = p + 1;
63 star_t = t;
64 p += 1;
65 } else if star_p != usize::MAX {
66 p = star_p;
67 star_t += 1;
68 t = star_t;
69 } else {
70 return false;
71 }
72 }
73
74 while p < pattern.len() && pattern[p] == b'*' {
76 p += 1;
77 }
78
79 p == pattern.len()
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 #[test]
87 fn glob_exact_match() {
88 assert!(glob_match("DB_URL", "DB_URL"));
89 assert!(!glob_match("DB_URL", "DB_HOST"));
90 }
91
92 #[test]
93 fn glob_star_wildcard() {
94 assert!(glob_match("DB_*", "DB_URL"));
95 assert!(glob_match("DB_*", "DB_HOST"));
96 assert!(glob_match("*_KEY", "API_KEY"));
97 assert!(glob_match("*_KEY", "SECRET_KEY"));
98 assert!(!glob_match("DB_*", "API_KEY"));
99 }
100
101 #[test]
102 fn glob_question_wildcard() {
103 assert!(glob_match("DB_UR?", "DB_URL"));
104 assert!(!glob_match("DB_UR?", "DB_URLS"));
105 }
106
107 #[test]
108 fn glob_case_insensitive() {
109 assert!(glob_match("db_url", "DB_URL"));
110 assert!(glob_match("DB_URL", "db_url"));
111 assert!(glob_match("Db_*", "DB_URL"));
112 }
113
114 #[test]
115 fn glob_star_matches_empty() {
116 assert!(glob_match("*", "ANYTHING"));
117 assert!(glob_match("*", ""));
118 assert!(glob_match("DB_*", "DB_"));
119 }
120
121 #[test]
122 fn glob_multiple_stars() {
123 assert!(glob_match("*DB*", "MY_DB_URL"));
124 assert!(glob_match("*_*_*", "A_B_C"));
125 }
126}