use anyhow::Result;
use std::path::PathBuf;
use std::process::{Command, Stdio};
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)
}
#[allow(dead_code)]
pub fn generate_python_wrapper(script_path: &str, locker_path: &std::path::Path) -> 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
)
}
#[allow(dead_code)]
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(())
}
#[allow(dead_code)]
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(())
}
const SHELL_MARKER_START: &str = "# >>> lazy-locker exports >>>";
const SHELL_MARKER_END: &str = "# <<< lazy-locker exports <<<";
pub fn generate_env_file(
store: &SecretsStore,
key: &[u8],
output_path: &std::path::PathBuf,
) -> Result<()> {
let secrets = store.decrypt_all(key)?;
let mut content = String::from("# Generated by lazy-locker\n");
content.push_str("# WARNING: This file contains secrets in plain text!\n");
content.push_str("# Do not commit this file to version control.\n\n");
for (name, mut value) in secrets {
let escaped_value = value.replace('\\', "\\\\").replace('"', "\\\"");
content.push_str(&format!("{}=\"{}\"\n", name, escaped_value));
value.zeroize();
}
std::fs::write(output_path, content)?;
Ok(())
}
pub fn export_to_shell_profile(
store: &SecretsStore,
key: &[u8],
shell: &str,
) -> Result<std::path::PathBuf> {
let home =
std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME environment variable not set"))?;
let profile_path = match shell {
"bash" => std::path::PathBuf::from(&home).join(".bashrc"),
"zsh" => std::path::PathBuf::from(&home).join(".zshrc"),
"fish" => std::path::PathBuf::from(&home).join(".config/fish/config.fish"),
_ => return Err(anyhow::anyhow!("Unsupported shell: {}", shell)),
};
let secrets = store.decrypt_all(key)?;
let mut exports = String::new();
exports.push_str(&format!("\n{}\n", SHELL_MARKER_START));
exports.push_str("# WARNING: Secrets in plain text - generated by lazy-locker\n");
for (name, mut value) in secrets {
let escaped_value = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$");
if shell == "fish" {
exports.push_str(&format!("set -gx {} \"{}\"\n", name, escaped_value));
} else {
exports.push_str(&format!("export {}=\"{}\"\n", name, escaped_value));
}
value.zeroize();
}
exports.push_str(&format!("{}\n", SHELL_MARKER_END));
let existing = std::fs::read_to_string(&profile_path).unwrap_or_default();
let cleaned = remove_shell_exports_from_content(&existing);
let new_content = format!("{}{}", cleaned.trim_end(), exports);
if let Some(parent) = profile_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&profile_path, new_content)?;
Ok(profile_path)
}
fn remove_shell_exports_from_content(content: &str) -> String {
let mut result = String::new();
let mut in_marker = false;
for line in content.lines() {
if line.trim() == SHELL_MARKER_START {
in_marker = true;
continue;
}
if line.trim() == SHELL_MARKER_END {
in_marker = false;
continue;
}
if !in_marker {
result.push_str(line);
result.push('\n');
}
}
result
}
pub fn clear_shell_exports() -> Result<Vec<std::path::PathBuf>> {
let home =
std::env::var("HOME").map_err(|_| anyhow::anyhow!("HOME environment variable not set"))?;
let profiles = [
std::path::PathBuf::from(&home).join(".bashrc"),
std::path::PathBuf::from(&home).join(".zshrc"),
std::path::PathBuf::from(&home).join(".config/fish/config.fish"),
];
let mut cleared = Vec::new();
for profile_path in profiles {
if profile_path.exists() {
let content = std::fs::read_to_string(&profile_path)?;
if content.contains(SHELL_MARKER_START) {
let cleaned = remove_shell_exports_from_content(&content);
std::fs::write(&profile_path, cleaned)?;
cleared.push(profile_path);
}
}
}
Ok(cleared)
}
pub fn export_to_json(
store: &SecretsStore,
key: &[u8],
output_path: &std::path::PathBuf,
) -> Result<()> {
let secrets = store.decrypt_all(key)?;
let json = serde_json::to_string_pretty(&secrets)?;
std::fs::write(output_path, json)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[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(),
&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(),
&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);
}
}