#![allow(dead_code)]
use super::permissions::{get_allow_rules, get_ask_rules, get_deny_rules};
use crate::types::permissions::{PermissionRule, PermissionRuleSource, ToolPermissionContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ShadowType {
Ask,
Deny,
}
pub struct UnreachableRule {
pub rule: PermissionRule,
pub reason: String,
pub shadowed_by: PermissionRule,
pub shadow_type: ShadowType,
pub fix: String,
}
pub struct DetectUnreachableRulesOptions {
pub sandbox_auto_allow_enabled: bool,
}
pub fn is_shared_setting_source(source: &PermissionRuleSource) -> bool {
matches!(
source,
PermissionRuleSource::ProjectSettings
| PermissionRuleSource::PolicySettings
| PermissionRuleSource::Command
)
}
fn format_source(source: &PermissionRuleSource) -> String {
source.as_str().to_string()
}
fn generate_fix_suggestion(
shadow_type: &ShadowType,
shadowing_rule: &PermissionRule,
shadowed_rule: &PermissionRule,
) -> String {
let shadowing_source = format_source(&shadowing_rule.source);
let shadowed_source = format_source(&shadowed_rule.source);
let tool_name = &shadowing_rule.rule_value.tool_name;
match shadow_type {
ShadowType::Deny => {
format!(
"Remove the \"{}\" deny rule from {}, or remove the specific allow rule from {}",
tool_name, shadowing_source, shadowed_source
)
}
ShadowType::Ask => {
format!(
"Remove the \"{}\" ask rule from {}, or remove the specific allow rule from {}",
tool_name, shadowing_source, shadowed_source
)
}
}
}
fn is_allow_rule_shadowed_by_ask_rule(
allow_rule: &PermissionRule,
ask_rules: &[PermissionRule],
options: &DetectUnreachableRulesOptions,
) -> Option<(PermissionRule, ShadowType)> {
let rule_content = &allow_rule.rule_value.rule_content;
if rule_content.is_none() {
return None;
}
let tool_name = &allow_rule.rule_value.tool_name;
let shadowing_ask_rule = ask_rules.iter().find(|ask_rule| {
ask_rule.rule_value.tool_name == *tool_name && ask_rule.rule_value.rule_content.is_none()
});
let Some(shadowing_ask_rule) = shadowing_ask_rule else {
return None;
};
if tool_name == "Bash" && options.sandbox_auto_allow_enabled {
if !is_shared_setting_source(&shadowing_ask_rule.source) {
return None;
}
}
Some((shadowing_ask_rule.clone(), ShadowType::Ask))
}
fn is_allow_rule_shadowed_by_deny_rule(
allow_rule: &PermissionRule,
deny_rules: &[PermissionRule],
) -> Option<(PermissionRule, ShadowType)> {
let rule_content = &allow_rule.rule_value.rule_content;
if rule_content.is_none() {
return None;
}
let tool_name = &allow_rule.rule_value.tool_name;
let shadowing_deny_rule = deny_rules.iter().find(|deny_rule| {
deny_rule.rule_value.tool_name == *tool_name && deny_rule.rule_value.rule_content.is_none()
});
shadowing_deny_rule.map(|rule| (rule.clone(), ShadowType::Deny))
}
pub fn detect_unreachable_rules(
context: &ToolPermissionContext,
options: &DetectUnreachableRulesOptions,
) -> Vec<UnreachableRule> {
let mut unreachable: Vec<UnreachableRule> = Vec::new();
let allow_rules = get_allow_rules(context);
let ask_rules = get_ask_rules(context);
let deny_rules = get_deny_rules(context);
for allow_rule in allow_rules {
if let Some((shadowing_rule, shadow_type)) =
is_allow_rule_shadowed_by_deny_rule(&allow_rule, &deny_rules)
{
let shadow_source = format_source(&shadowing_rule.source);
unreachable.push(UnreachableRule {
rule: allow_rule.clone(),
reason: format!(
"Blocked by \"{}\" deny rule (from {})",
shadowing_rule.rule_value.tool_name, shadow_source
),
shadowed_by: shadowing_rule.clone(),
shadow_type,
fix: generate_fix_suggestion(&shadow_type, &shadowing_rule, &allow_rule),
});
continue;
}
if let Some((shadowing_rule, shadow_type)) =
is_allow_rule_shadowed_by_ask_rule(&allow_rule, &ask_rules, options)
{
let shadow_source = format_source(&shadowing_rule.source);
unreachable.push(UnreachableRule {
rule: allow_rule.clone(),
reason: format!(
"Shadowed by \"{}\" ask rule (from {})",
shadowing_rule.rule_value.tool_name, shadow_source
),
shadowed_by: shadowing_rule.clone(),
shadow_type,
fix: generate_fix_suggestion(&shadow_type, &shadowing_rule, &allow_rule),
});
}
}
unreachable
}