use std::path::PathBuf;
use std::process::{Command, Stdio};
use anyhow::Result;
use zeroize::Zeroize;
use crate::core::store::SecretsStore;
#[derive(Debug, Clone)]
pub struct TokenUsage {
pub file_path: String,
pub line_number: usize,
pub line_content: String,
}
pub fn find_token_usages(token_name: &str, search_dir: &PathBuf) -> Vec<TokenUsage> {
let mut usages = Vec::new();
let patterns = vec![
format!("${{{}}}", token_name), format!("${}", token_name), format!("process.env.{}", token_name), format!("os.environ[\"{}\"]", token_name), format!("os.getenv(\"{}\")", token_name), format!("ENV[\"{}\"]", token_name), token_name.to_string(), ];
if let Ok(entries) = walkdir(search_dir) {
for entry in entries {
if let Ok(content) = std::fs::read_to_string(&entry) {
for (line_num, line) in content.lines().enumerate() {
for pattern in &patterns {
if line.contains(pattern) {
usages.push(TokenUsage {
file_path: entry.to_string_lossy().to_string(),
line_number: line_num + 1,
line_content: line.trim().to_string(),
});
break; }
}
}
}
}
}
usages
}
fn walkdir(dir: &PathBuf) -> Result<Vec<PathBuf>> {
let mut files = Vec::new();
if !dir.is_dir() {
return Ok(files);
}
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.') || name == "node_modules" || name == "target" || name == "__pycache__" {
continue;
}
if path.is_dir() {
files.extend(walkdir(&path)?);
} else {
let ext = path.extension().unwrap_or_default().to_string_lossy();
if matches!(ext.as_ref(), "py" | "js" | "ts" | "jsx" | "tsx" | "sh" | "bash" | "env" | "yaml" | "yml" | "json" | "toml" | "rb" | "go" | "rs") {
files.push(path);
}
}
}
Ok(files)
}
pub fn execute_with_secrets(
command: &str,
store: &SecretsStore,
key: &[u8],
) -> Result<std::process::Output> {
let mut env_vars = store.decrypt_all(key)?;
let output = Command::new("sh")
.arg("-c")
.arg(command)
.envs(&env_vars)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()?;
for (_, mut value) in env_vars.drain() {
value.zeroize();
}
Ok(output)
}
pub fn generate_python_wrapper(script_path: &str, locker_path: &PathBuf) -> String {
format!(r#"#!/usr/bin/env python3
"""
Secure wrapper generated by lazy-locker.
This script injects secrets in memory before executing the target script.
"""
import subprocess
import sys
import os
def main():
# Call lazy-locker to get secrets (via secure pipe)
result = subprocess.run(
['lazy-locker', 'export', '--format', 'env'],
capture_output=True,
text=True,
cwd='{locker_dir}'
)
if result.returncode != 0:
print("Error: Unable to load secrets", file=sys.stderr)
sys.exit(1)
# Parse environment variables
env = os.environ.copy()
for line in result.stdout.strip().split('\n'):
if '=' in line:
key, value = line.split('=', 1)
env[key] = value
# Execute target script with injected secrets
subprocess.run([sys.executable, '{script}'] + sys.argv[1:], env=env)
if __name__ == '__main__':
main()
"#,
locker_dir = locker_path.display(),
script = script_path
)
}
pub fn generate_env_reference(store: &SecretsStore, output_path: &PathBuf) -> Result<()> {
let mut content = String::from("# File generated by lazy-locker\n");
content.push_str("# Values are stored securely in the locker.\n");
content.push_str("# Use 'lazy-locker run <command>' to execute with secrets.\n\n");
for secret in store.list_secrets() {
let expiration = secret.expiration_display();
content.push_str(&format!("# {} - {}\n", secret.name, expiration));
content.push_str(&format!("{}=${{LAZY_LOCKER:{}}}\n\n", secret.name, secret.name));
}
std::fs::write(output_path, content)?;
Ok(())
}
pub fn export_env_format(store: &SecretsStore, key: &[u8]) -> Result<String> {
let secrets = store.decrypt_all(key)?;
let mut output = String::new();
for (name, mut value) in secrets {
let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\"");
output.push_str(&format!("{}=\"{}\"\n", name, escaped_value));
value.zeroize();
}
Ok(output)
}
pub fn copy_to_clipboard(value: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
let result = Command::new("xclip")
.args(["-selection", "clipboard"])
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
if result.is_ok() {
return Ok(());
}
let result = Command::new("xsel")
.args(["--clipboard", "--input"])
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
if result.is_ok() {
return Ok(());
}
let result = Command::new("wl-copy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
});
result.map_err(|_| anyhow::anyhow!("No clipboard tool available (xclip, xsel, wl-copy)"))?;
}
#[cfg(target_os = "macos")]
{
Command::new("pbcopy")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
})
.map_err(|e| anyhow::anyhow!("pbcopy error: {}", e))?;
}
#[cfg(target_os = "windows")]
{
Command::new("clip")
.stdin(Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(ref mut stdin) = child.stdin {
stdin.write_all(value.as_bytes())?;
}
child.wait()
})
.map_err(|e| anyhow::anyhow!("clip error: {}", e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use std::fs;
#[test]
fn test_walkdir_finds_supported_files() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(temp_dir.path().join("script.py"), "print('hello')").unwrap();
fs::write(temp_dir.path().join("app.js"), "console.log('hi')").unwrap();
fs::write(temp_dir.path().join("config.json"), "{}").unwrap();
fs::write(temp_dir.path().join("readme.txt"), "ignored").unwrap();
let files = walkdir(&temp_dir.path().to_path_buf()).expect("walkdir failed");
assert_eq!(files.len(), 3); }
#[test]
fn test_walkdir_ignores_hidden_dirs() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let hidden_dir = temp_dir.path().join(".hidden");
fs::create_dir(&hidden_dir).unwrap();
fs::write(hidden_dir.join("secret.py"), "hidden").unwrap();
fs::write(temp_dir.path().join("visible.py"), "visible").unwrap();
let files = walkdir(&temp_dir.path().to_path_buf()).expect("walkdir failed");
assert_eq!(files.len(), 1);
assert!(files[0].to_string_lossy().contains("visible.py"));
}
#[test]
fn test_walkdir_ignores_node_modules() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let node_modules = temp_dir.path().join("node_modules");
fs::create_dir(&node_modules).unwrap();
fs::write(node_modules.join("dep.js"), "dependency").unwrap();
fs::write(temp_dir.path().join("index.js"), "main").unwrap();
let files = walkdir(&temp_dir.path().to_path_buf()).expect("walkdir failed");
assert_eq!(files.len(), 1);
assert!(files[0].to_string_lossy().contains("index.js"));
}
#[test]
fn test_walkdir_recursive() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let src = temp_dir.path().join("src");
let deep = src.join("deep").join("nested");
fs::create_dir_all(&deep).unwrap();
fs::write(temp_dir.path().join("root.py"), "root").unwrap();
fs::write(src.join("module.py"), "module").unwrap();
fs::write(deep.join("helper.py"), "helper").unwrap();
let files = walkdir(&temp_dir.path().to_path_buf()).expect("walkdir failed");
assert_eq!(files.len(), 3);
}
#[test]
fn test_walkdir_empty_directory() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let files = walkdir(&temp_dir.path().to_path_buf()).expect("walkdir failed");
assert!(files.is_empty());
}
#[test]
fn test_walkdir_nonexistent_directory() {
let nonexistent = PathBuf::from("/nonexistent/path/12345");
let files = walkdir(&nonexistent).expect("walkdir should not fail");
assert!(files.is_empty());
}
#[test]
fn test_find_token_usages_env_variable_syntax() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("script.sh"),
"echo $MY_API_KEY\necho ${MY_API_KEY}"
).unwrap();
let usages = find_token_usages("MY_API_KEY", &temp_dir.path().to_path_buf());
assert!(!usages.is_empty());
assert!(usages.iter().any(|u| u.line_content.contains("$MY_API_KEY")));
}
#[test]
fn test_find_token_usages_python_syntax() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("app.py"),
r#"import os
api_key = os.environ["DB_PASSWORD"]
other = os.getenv("DB_PASSWORD")
"#
).unwrap();
let usages = find_token_usages("DB_PASSWORD", &temp_dir.path().to_path_buf());
assert!(usages.len() >= 2);
}
#[test]
fn test_find_token_usages_javascript_syntax() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("server.js"),
"const key = process.env.SECRET_KEY;"
).unwrap();
let usages = find_token_usages("SECRET_KEY", &temp_dir.path().to_path_buf());
assert_eq!(usages.len(), 1);
assert!(usages[0].line_content.contains("process.env.SECRET_KEY"));
}
#[test]
fn test_find_token_usages_no_matches() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("clean.py"),
"print('no secrets here')"
).unwrap();
let usages = find_token_usages("NONEXISTENT_TOKEN", &temp_dir.path().to_path_buf());
assert!(usages.is_empty());
}
#[test]
fn test_find_token_usages_includes_line_info() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
fs::write(
temp_dir.path().join("config.py"),
"# Comment\nTOKEN = $API_KEY\n# End"
).unwrap();
let usages = find_token_usages("API_KEY", &temp_dir.path().to_path_buf());
assert_eq!(usages.len(), 1);
assert_eq!(usages[0].line_number, 2);
assert!(usages[0].file_path.contains("config.py"));
}
#[test]
fn test_generate_env_reference_creates_file() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key = [0x42u8; 32];
let mut store = crate::core::store::SecretsStore::new();
store.add_secret(
"TEST_VAR".to_string(),
"value".to_string(),
None,
&temp_dir.path().to_path_buf(),
&key,
).expect("Failed to add secret");
let output_path = temp_dir.path().join(".env.encrypted");
generate_env_reference(&store, &output_path).expect("Failed to generate reference");
assert!(output_path.exists());
let content = fs::read_to_string(&output_path).unwrap();
assert!(content.contains("TEST_VAR"));
assert!(content.contains("LAZY_LOCKER:TEST_VAR"));
assert!(content.contains("# File generated by lazy-locker"));
}
#[test]
fn test_generate_env_reference_no_plaintext_values() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let key = [0x42u8; 32];
let mut store = crate::core::store::SecretsStore::new();
let secret_value = "super_secret_password_123";
store.add_secret(
"PASSWORD".to_string(),
secret_value.to_string(),
None,
&temp_dir.path().to_path_buf(),
&key,
).expect("Failed to add secret");
let output_path = temp_dir.path().join(".env.ref");
generate_env_reference(&store, &output_path).expect("Failed to generate reference");
let content = fs::read_to_string(&output_path).unwrap();
assert!(!content.contains(secret_value));
assert!(content.contains("${LAZY_LOCKER:PASSWORD}"));
}
#[test]
fn test_generate_python_wrapper_structure() {
let locker_path = PathBuf::from("/home/user/.lazy-locker");
let wrapper = generate_python_wrapper("app.py", &locker_path);
assert!(wrapper.contains("#!/usr/bin/env python3"));
assert!(wrapper.contains("lazy-locker"));
assert!(wrapper.contains("app.py"));
assert!(wrapper.contains("/home/user/.lazy-locker"));
assert!(wrapper.contains("def main()"));
}
#[test]
fn test_token_usage_clone() {
let usage = TokenUsage {
file_path: "src/main.py".to_string(),
line_number: 42,
line_content: "api_key = os.environ['KEY']".to_string(),
};
let cloned = usage.clone();
assert_eq!(cloned.file_path, usage.file_path);
assert_eq!(cloned.line_number, usage.line_number);
assert_eq!(cloned.line_content, usage.line_content);
}
}