use crate::config::GuardrailsConfig;
use crate::errors::{RalphError, Result};
use regex::Regex;
use serde_json::Value;
use std::path::PathBuf;
static SECRET_PATTERNS: &[&str] = &[
r"(?i)(?:api[_-]?key|secret[_-]?key|access[_-]?key)\s*[:=]\s*[A-Za-z0-9+/]{20,}",
r"(?i)password\s*[:=]\s*[a-zA-Z0-9!@#$%^&*]{8,}",
r"sk-[A-Za-z0-9]{32,}",
r"AKIA[0-9A-Z]{16}",
r"(?i)anthropic[_-]?key\s*[:=]\s*sk-ant-[A-Za-z0-9]{20,}",
r"-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----",
];
pub struct GuardrailChecker {
workspace: PathBuf,
config: GuardrailsConfig,
secret_patterns: Vec<Regex>,
}
impl GuardrailChecker {
pub fn new(workspace: PathBuf, config: GuardrailsConfig) -> Self {
let secret_patterns = SECRET_PATTERNS
.iter()
.filter_map(|p| Regex::new(p).ok())
.collect();
Self {
workspace,
config,
secret_patterns,
}
}
pub fn check_tool_call(&self, tool_name: &str, args: &Value) -> Result<()> {
match tool_name {
"write_file" | "edit_file" => {
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
self.check_path(path)?;
}
}
"read_file" | "list_dir" | "delete_file" => {
if let Some(path) = args.get("path").and_then(|v| v.as_str()) {
self.check_path(path)?;
}
}
"run_command" => {
if let Some(cmd) = args.get("cmd").and_then(|v| v.as_str()) {
self.check_command(cmd)?;
}
if let Some(cwd) = args.get("cwd").and_then(|v| v.as_str()) {
self.check_path(cwd)?;
}
}
_ => {}
}
Ok(())
}
pub fn check_path(&self, path: &str) -> Result<()> {
if path.contains("..") {
let joined = self.workspace.join(path);
let normalized = normalize_path(&joined);
let ws_norm = normalize_path(&self.workspace);
if !normalized.starts_with(&ws_norm) {
return Err(RalphError::PathEscape(path.to_string()));
}
}
if std::path::Path::new(path).is_absolute() {
let p = PathBuf::from(path);
let ws = self
.workspace
.canonicalize()
.unwrap_or_else(|_| self.workspace.clone());
if !p.starts_with(&ws) {
return Err(RalphError::PathEscape(path.to_string()));
}
}
Ok(())
}
pub fn check_command(&self, cmd: &str) -> Result<()> {
for blocked in &self.config.blocked_commands {
let normalized = cmd
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase();
let block_norm = blocked
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase();
if normalized.contains(&block_norm) {
return Err(RalphError::BlockedCommand(cmd.to_string()));
}
}
Ok(())
}
pub fn check_content_for_secrets(&self, content: &str) -> Result<()> {
for pattern in &self.secret_patterns {
if pattern.is_match(content) {
return Err(RalphError::SecretDetected);
}
}
Ok(())
}
}
fn normalize_path(path: &PathBuf) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {}
c => components.push(c),
}
}
components.iter().collect()
}