use chio_kernel::{GuardContext, KernelError, Verdict};
use glob::Pattern;
use crate::action::{extract_action, ToolAction};
use crate::path_normalization::{
normalize_path_for_policy, normalize_path_for_policy_lexical_absolute,
normalize_path_for_policy_with_fs,
};
fn default_forbidden_patterns() -> Vec<String> {
let mut patterns = vec![
"**/.ssh/**".to_string(),
"**/id_rsa*".to_string(),
"**/id_ed25519*".to_string(),
"**/id_ecdsa*".to_string(),
"**/.aws/**".to_string(),
"**/.env".to_string(),
"**/.env.*".to_string(),
"**/.git-credentials".to_string(),
"**/.gitconfig".to_string(),
"**/.gnupg/**".to_string(),
"**/.kube/**".to_string(),
"**/.docker/**".to_string(),
"**/.npmrc".to_string(),
"**/.password-store/**".to_string(),
"**/pass/**".to_string(),
"**/.1password/**".to_string(),
"/etc/shadow".to_string(),
"/etc/passwd".to_string(),
"/etc/sudoers".to_string(),
];
patterns.extend([
"**/AppData/Roaming/Microsoft/Credentials/**".to_string(),
"**/AppData/Local/Microsoft/Credentials/**".to_string(),
"**/AppData/Roaming/Microsoft/Vault/**".to_string(),
"**/NTUSER.DAT".to_string(),
"**/NTUSER.DAT.*".to_string(),
"**/Windows/System32/config/SAM".to_string(),
"**/Windows/System32/config/SECURITY".to_string(),
"**/Windows/System32/config/SYSTEM".to_string(),
"**/*.reg".to_string(),
"**/AppData/Roaming/Microsoft/SystemCertificates/**".to_string(),
"**/WindowsPowerShell/profile.ps1".to_string(),
"**/PowerShell/profile.ps1".to_string(),
]);
patterns
}
pub struct ForbiddenPathGuard {
patterns: Vec<Pattern>,
exceptions: Vec<Pattern>,
}
impl ForbiddenPathGuard {
pub fn new() -> Self {
Self::with_patterns(default_forbidden_patterns(), vec![])
}
pub fn with_patterns(patterns: Vec<String>, exceptions: Vec<String>) -> Self {
let patterns = patterns
.iter()
.filter_map(|p| Pattern::new(p).ok())
.collect();
let exceptions = exceptions
.iter()
.filter_map(|p| Pattern::new(p).ok())
.collect();
Self {
patterns,
exceptions,
}
}
pub fn is_forbidden(&self, path: &str) -> bool {
let lexical_path = normalize_path_for_policy(path);
let resolved_path = normalize_path_for_policy_with_fs(path);
let lexical_abs_path = normalize_path_for_policy_lexical_absolute(path);
let resolved_differs_from_lexical_target = lexical_abs_path
.as_deref()
.map(|abs| abs != resolved_path.as_str())
.unwrap_or(resolved_path != lexical_path);
for exception in &self.exceptions {
let lexical_matches = exception.matches(&lexical_path)
|| lexical_abs_path
.as_deref()
.map(|abs| exception.matches(abs))
.unwrap_or(false);
let resolved_matches = exception.matches(&resolved_path);
let exception_matches = if resolved_differs_from_lexical_target {
resolved_matches
} else {
resolved_matches || lexical_matches
};
if exception_matches {
return false;
}
}
for pattern in &self.patterns {
if pattern.matches(&resolved_path) || pattern.matches(&lexical_path) {
return true;
}
}
false
}
}
impl Default for ForbiddenPathGuard {
fn default() -> Self {
Self::new()
}
}
impl chio_kernel::Guard for ForbiddenPathGuard {
fn name(&self) -> &str {
"forbidden-path"
}
fn evaluate(&self, ctx: &GuardContext) -> Result<Verdict, KernelError> {
let action = extract_action(&ctx.request.tool_name, &ctx.request.arguments);
let path = match &action {
ToolAction::FileAccess(p) | ToolAction::FileWrite(p, _) | ToolAction::Patch(p, _) => {
Some(p.as_str())
}
_ => None,
};
let Some(path) = path else {
return Ok(Verdict::Allow);
};
if self.is_forbidden(path) {
Ok(Verdict::Deny)
} else {
Ok(Verdict::Allow)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_ssh_keys() {
let guard = ForbiddenPathGuard::new();
assert!(guard.is_forbidden("/home/user/.ssh/id_rsa"));
assert!(guard.is_forbidden("/home/user/.ssh/authorized_keys"));
}
#[test]
fn blocks_etc_shadow() {
let guard = ForbiddenPathGuard::new();
assert!(guard.is_forbidden("/etc/shadow"));
}
#[test]
fn blocks_aws_credentials() {
let guard = ForbiddenPathGuard::new();
assert!(guard.is_forbidden("/home/user/.aws/credentials"));
}
#[test]
fn blocks_env_files() {
let guard = ForbiddenPathGuard::new();
assert!(guard.is_forbidden("/app/.env"));
assert!(guard.is_forbidden("/app/.env.local"));
}
#[test]
fn allows_normal_files() {
let guard = ForbiddenPathGuard::new();
assert!(!guard.is_forbidden("/home/user/project/src/main.rs"));
assert!(!guard.is_forbidden("/home/user/project/README.md"));
assert!(!guard.is_forbidden("/app/src/main.rs"));
}
#[test]
fn exceptions_work() {
let guard = ForbiddenPathGuard::with_patterns(
vec!["**/.env".to_string()],
vec!["**/project/.env".to_string()],
);
assert!(guard.is_forbidden("/app/.env"));
assert!(!guard.is_forbidden("/app/project/.env"));
}
#[test]
fn windows_paths_normalized() {
let guard = ForbiddenPathGuard::new();
assert!(guard.is_forbidden(r"C:\Users\alice\.ssh\id_rsa"));
assert!(guard.is_forbidden(r"C:\Users\bob\.aws\credentials"));
assert!(!guard.is_forbidden(r"C:\Users\alice\Documents\report.docx"));
}
}