use std::collections::HashMap;
use std::path::PathBuf;
const FORBIDDEN_PATTERNS: &[(&str, &str)] = &[
("GH_TOKEN env", "GH_TOKEN\\s*=\\s*[\"']?gh[pousr]_"),
("GITHUB_TOKEN env", "GITHUB_TOKEN\\s*=\\s*[\"']?gh[pousr]_"),
("GitHub PAT", "github_pat_[A-Za-z0-9_]{22,}"),
("GitHub token", "gh[pousr]_[A-Za-z0-9_]{36,}"),
("AWS key", "AKIA[0-9A-Z]{16}"),
(
"Private key header",
"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
),
("OpenAI key", "sk-[A-Za-z0-9\\-]{20,}"),
("Anthropic key", "sk-ant-[A-Za-z0-9\\-]{20,}"),
];
fn scan_for_secrets(content: &str) -> Vec<(usize, String, String)> {
let mut findings = Vec::new();
for (line_num, line) in content.lines().enumerate() {
for (name, pattern) in FORBIDDEN_PATTERNS {
let regex = regex::Regex::new(pattern).expect("invalid regex pattern");
if regex.is_match(line) {
findings.push((line_num + 1, (*name).to_string(), line.trim().to_string()));
}
}
}
findings
}
fn assert_no_secrets_in_files(files: &[(PathBuf, String)]) {
let mut all_violations = Vec::new();
for (path, content) in files {
let violations = scan_for_secrets(content);
for (line, pattern_name, snippet) in violations {
all_violations.push((path.clone(), line, pattern_name, snippet));
}
}
if !all_violations.is_empty() {
let mut msg = String::from("Secret leakage detected in task artifacts:\n\n");
for (path, line, pattern, snippet) in all_violations {
msg.push_str(&format!(
" File: {}\n Line: {}\n Pattern: {}\n Snippet: {}\n\n",
path.display(),
line,
pattern,
&snippet[..snippet.len().min(100)]
));
}
panic!("{}", msg);
}
}
fn setup_temp_home() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().expect("failed to create temp dir");
let home = temp.path().join("home");
std::fs::create_dir_all(&home).expect("failed to create home dir");
std::env::set_var("HOME", &home);
(temp, home)
}
mod unit_tests {
use super::*;
#[test]
fn test_scan_detects_github_token() {
let content = r#"
export GH_TOKEN="ghp_abcdefghijklmnopqrstuvwxyz1234"
"#;
let findings = scan_for_secrets(content);
assert!(!findings.is_empty(), "Should detect GH_TOKEN with ghp_");
assert!(findings.iter().any(|(_, name, _)| name == "GH_TOKEN env"));
}
#[test]
fn test_scan_detects_private_key() {
let content = r#"
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MqK8k7f5l2EckKlw
-----END RSA PRIVATE KEY-----
"#;
let findings = scan_for_secrets(content);
assert!(!findings.is_empty(), "Should detect private key");
assert!(findings
.iter()
.any(|(_, name, _)| name == "Private key header"));
}
#[test]
fn test_scan_ignores_safe_content() {
let content = r#"
# Normal task instructions
Fix the bug in src/main.rs
Add tests for the new feature
"#;
let findings = scan_for_secrets(content);
assert!(findings.is_empty(), "Should not flag safe content");
}
#[test]
fn test_prompt_files_no_secrets() {
let (_temp, _home) = setup_temp_home();
let sys_prompt = r#"# Task Instructions
You are an AI assistant helping with a software engineering task.
## Guidelines
- Write clean, maintainable code
- Follow existing patterns in the codebase
- Add tests for new functionality
## Task
Fix the bug in the authentication module."#;
let msg_prompt = r#"# Task #123: Fix auth bug
The authentication module has a bug where tokens are not validated correctly.
## Expected Behavior
Tokens should be validated against the database.
## Current Behavior
Tokens are accepted without validation."#;
let attempt_dir = _home.join(".orch/state/owner/repo/tasks/123/attempts/1");
std::fs::create_dir_all(&attempt_dir).unwrap();
let sys_file = attempt_dir.join("prompt-sys.md");
let msg_file = attempt_dir.join("prompt-msg.md");
std::fs::write(&sys_file, sys_prompt).unwrap();
std::fs::write(&msg_file, msg_prompt).unwrap();
let files = vec![
(sys_file, sys_prompt.to_string()),
(msg_file, msg_prompt.to_string()),
];
assert_no_secrets_in_files(&files);
}
#[test]
fn test_safe_patterns_not_flagged() {
let content = r#"#!/usr/bin/env bash
set -euo pipefail
# These are safe patterns that should NOT be flagged:
export TASK_ID="123"
export GIT_AUTHOR_NAME="Test User"
export GIT_COMMITTER_EMAIL="test@example.com"
# Comments mentioning tokens in documentation context:
# The GH_TOKEN environment variable is used for authentication
# See: https://docs.github.com/en/authentication
# Variable names without actual values:
echo "$GH_TOKEN" # This references an env var, doesn't leak it
echo "${GITHUB_TOKEN:-}" # This references an env var with default
cd /work/dir
claude -p --system prompt-sys.md < prompt-msg.md
"#;
let findings = scan_for_secrets(content);
assert!(
findings.is_empty(),
"Should not flag safe patterns: {:?}",
findings
);
}
}
mod integration_tests {
use super::*;
#[test]
fn test_runner_script_with_env_token() {
let (_temp, home) = setup_temp_home();
let test_token = "ghp_test1234567890abcdefghijklmnopqrstuvwxyz12";
std::env::set_var("GH_TOKEN", test_token);
let attempt_dir = home.join(".orch/state/owner/repo/tasks/456/attempts/1");
std::fs::create_dir_all(&attempt_dir).unwrap();
let sys_file = attempt_dir.join("prompt-sys.md");
let msg_file = attempt_dir.join("prompt-msg.md");
let sys_prompt = "System prompt without secrets".to_string();
let msg_prompt = "Message prompt without secrets".to_string();
std::fs::write(&sys_file, &sys_prompt).unwrap();
std::fs::write(&msg_file, &msg_prompt).unwrap();
let runner_script = format!(
r#"#!/usr/bin/env bash
set -euo pipefail
export GH_TOKEN="{}"
export GIT_AUTHOR_NAME="Test User"
export GIT_AUTHOR_EMAIL="test@example.com"
"#,
test_token
);
let runner_file = attempt_dir.join("runner.sh");
std::fs::write(&runner_file, &runner_script).unwrap();
let prompt_files = vec![
(sys_file.clone(), sys_prompt),
(msg_file.clone(), msg_prompt),
];
assert_no_secrets_in_files(&prompt_files);
std::env::remove_var("GH_TOKEN");
}
#[test]
fn test_runner_script_without_env_token() {
let (_temp, home) = setup_temp_home();
std::env::remove_var("GH_TOKEN");
std::env::remove_var("GITHUB_TOKEN");
let attempt_dir = home.join(".orch/state/owner/repo/tasks/789/attempts/1");
std::fs::create_dir_all(&attempt_dir).unwrap();
let runner_script = r#"#!/usr/bin/env bash
set -euo pipefail
# No GH_TOKEN available - agent will need to authenticate via other means
export GIT_AUTHOR_NAME="Test User"
export GIT_AUTHOR_EMAIL="test@example.com"
# Source user private file if it exists
[ -f "$HOME/.private" ] && source "$HOME/.private"
"#;
let runner_file = attempt_dir.join("runner.sh");
std::fs::write(&runner_file, runner_script).unwrap();
let files = vec![(runner_file, runner_script.to_string())];
assert_no_secrets_in_files(&files);
}
#[test]
fn test_full_attempt_directory_scan() {
let (_temp, home) = setup_temp_home();
let attempt_dir = home.join(".orch/state/owner/repo/tasks/999/attempts/1");
std::fs::create_dir_all(&attempt_dir).unwrap();
let files = vec![
(
"prompt-sys.md",
r#"# System Prompt
You are a helpful AI assistant.
## Task
Fix the authentication bug in src/auth.rs."#,
),
(
"prompt-msg.md",
r#"# Task #999
The auth module needs fixing."#,
),
(
"runner.sh",
r#"#!/usr/bin/env bash
set -euo pipefail
export PATH="/opt/homebrew/bin:$PATH"
export GIT_AUTHOR_NAME="Test User"
export GIT_COMMITTER_NAME="Test User"
export GIT_AUTHOR_EMAIL="test@example.com"
export GIT_COMMITTER_EMAIL="test@example.com"
export TASK_ID="999"
export OUTPUT_FILE="/output.json"
unset CLAUDECODE
cd "/work/dir"
# Run agent
RESPONSE=$(claude -p --system prompt-sys.md < prompt-msg.md) || CMD_STATUS=$?
CMD_STATUS=${CMD_STATUS:-0}
printf '%s' "$RESPONSE" > "$OUTPUT_FILE"
echo "$CMD_STATUS" > exit.txt
exit $CMD_STATUS
"#,
),
("output.json", r#"{"status":"done","summary":"Fixed auth"}"#),
("exit.txt", "0"),
];
let mut file_contents = Vec::new();
for (filename, content) in &files {
let path = attempt_dir.join(filename);
std::fs::write(&path, content).unwrap();
file_contents.push((path, (*content).to_string()));
}
assert_no_secrets_in_files(&file_contents);
}
#[test]
fn test_template_rendering_no_leaks() {
let mut vars = HashMap::new();
vars.insert("ROLE".to_string(), "developer".to_string());
vars.insert("TASK_ID".to_string(), "123".to_string());
vars.insert("TASK_TITLE".to_string(), "Fix bug".to_string());
vars.insert("TASK_BODY".to_string(), "The bug is in main.rs".to_string());
let template_output = format!(
r#"Role: {}
Task: {} - {}
Description: {}
"#,
vars.get("ROLE").unwrap(),
vars.get("TASK_ID").unwrap(),
vars.get("TASK_TITLE").unwrap(),
vars.get("TASK_BODY").unwrap(),
);
let findings = scan_for_secrets(&template_output);
assert!(
findings.is_empty(),
"Template output should not contain secrets"
);
}
}
#[test]
fn test_security_module_integration() {
let content_with_secrets = r#"
export GH_TOKEN=ghp_abcdefghijklmnopqrstuvwxyz123456
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF8PbnGy0AHB7MqK8k7f5l2EckKlw
"#;
let clean_content = r#"
# Normal task instructions
Fix the bug in src/main.rs
Add tests for the new feature
"#;
let secret_findings = scan_for_secrets(content_with_secrets);
assert!(
!secret_findings.is_empty(),
"Should detect secrets in content"
);
let clean_findings = scan_for_secrets(clean_content);
assert!(clean_findings.is_empty(), "Should not flag clean content");
}