use std::io::ErrorKind; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; use arc_swap::ArcSwap; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigLayerStackOrdering; use crate::is_dangerous_command::command_might_be_dangerous; use crate::is_safe_command::is_known_safe_command; use codex_execpolicy::AmendError; use codex_execpolicy::Decision; use codex_execpolicy::Error as ExecPolicyRuleError; use codex_execpolicy::Evaluation; use codex_execpolicy::MatchOptions; use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; use codex_execpolicy::PolicyParser; use codex_execpolicy::RuleMatch; use codex_execpolicy::blocking_append_allow_prefix_rule; use codex_execpolicy::blocking_append_network_rule; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::SandboxPolicy; use thiserror::Error; use tokio::fs; use tokio::task::spawn_blocking; use tracing::instrument; use crate::bash::parse_shell_lc_plain_commands; use crate::bash::parse_shell_lc_single_command_prefix; use crate::config::Config; use crate::sandboxing::SandboxPermissions; use crate::tools::sandboxing::ExecApprovalRequirement; use codex_utils_absolute_path::AbsolutePathBuf; use shlex::try_join as shlex_try_join; const PROMPT_CONFLICT_REASON: &str = "approval required by policy, but AskForApproval is set to Never"; const REJECT_SANDBOX_APPROVAL_REASON: &str = "approval required by policy, but AskForApproval::Granular.sandbox_approval is false"; const REJECT_RULES_APPROVAL_REASON: &str = "approval required by policy rule, but AskForApproval::Granular.rules is false"; const RULES_DIR_NAME: &str = "rules"; const RULE_EXTENSION: &str = "rules"; const DEFAULT_POLICY_FILE: &str = "default.rules"; static BANNED_PREFIX_SUGGESTIONS: &[&[&str]] = &[ &["python3"], &["python3", "-"], &["python3", "-c"], &["python"], &["python", "-"], &["python", "-c"], &["py"], &["py", "-3"], &["pythonw"], &["pyw"], &["pypy"], &["pypy3"], &["git"], &["bash"], &["bash", "-lc"], &["sh"], &["sh", "-c"], &["sh", "-lc"], &["zsh"], &["zsh", "-lc"], &["/bin/zsh"], &["/bin/zsh", "-lc"], &["/bin/bash"], &["/bin/bash", "-lc"], &["pwsh"], &["pwsh", "-Command"], &["pwsh", "-c"], &["powershell"], &["powershell", "-Command"], &["powershell", "-c"], &["powershell.exe"], &["powershell.exe", "-Command"], &["powershell.exe", "-c"], &["env"], &["sudo"], &["node"], &["node", "-e"], &["perl"], &["perl", "-e"], &["ruby"], &["ruby", "-e"], &["php"], &["php", "-r"], &["lua"], &["lua", "-e"], &["osascript"], ]; pub(crate) fn child_uses_parent_exec_policy(parent_config: &Config, child_config: &Config) -> bool { fn exec_policy_config_folders(config: &Config) -> Vec { config .config_layer_stack .get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ) .into_iter() .filter_map(codex_config::ConfigLayerEntry::config_folder) .collect() } exec_policy_config_folders(parent_config) == exec_policy_config_folders(child_config) && parent_config.config_layer_stack.requirements().exec_policy == child_config.config_layer_stack.requirements().exec_policy } fn is_policy_match(rule_match: &RuleMatch) -> bool { match rule_match { RuleMatch::PrefixRuleMatch { .. } => true, RuleMatch::HeuristicsRuleMatch { .. } => false, } } /// Returns a rejection reason when `approval_policy` disallows surfacing the /// current prompt to the user. /// /// `prompt_is_rule` distinguishes policy-rule prompts from sandbox/escalation /// prompts so granular `rules` and `sandbox_approval` settings are honored /// independently. When both are present, policy-rule prompts take precedence. pub(crate) fn prompt_is_rejected_by_policy( approval_policy: AskForApproval, prompt_is_rule: bool, ) -> Option<&'static str> { match approval_policy { AskForApproval::Never => Some(PROMPT_CONFLICT_REASON), AskForApproval::OnFailure => None, AskForApproval::OnRequest => None, AskForApproval::UnlessTrusted => None, AskForApproval::Granular(granular_config) => { if prompt_is_rule { if !granular_config.allows_rules_approval() { Some(REJECT_RULES_APPROVAL_REASON) } else { None } } else if !granular_config.allows_sandbox_approval() { Some(REJECT_SANDBOX_APPROVAL_REASON) } else { None } } } } #[derive(Debug, Error)] pub enum ExecPolicyError { #[error("failed to read rules files from {dir}: {source}")] ReadDir { dir: PathBuf, source: std::io::Error, }, #[error("failed to read rules file {path}: {source}")] ReadFile { path: PathBuf, source: std::io::Error, }, #[error("failed to parse rules file {path}: {source}")] ParsePolicy { path: String, source: codex_execpolicy::Error, }, } #[derive(Debug, Error)] pub enum ExecPolicyUpdateError { #[error("failed to update rules file {path}: {source}")] AppendRule { path: PathBuf, source: AmendError }, #[error("failed to join blocking rules update task: {source}")] JoinBlockingTask { source: tokio::task::JoinError }, #[error("failed to update in-memory rules: {source}")] AddRule { #[from] source: ExecPolicyRuleError, }, } pub(crate) struct ExecPolicyManager { policy: ArcSwap, update_lock: tokio::sync::Mutex<()>, } pub(crate) struct ExecApprovalRequest<'a> { pub(crate) command: &'a [String], pub(crate) approval_policy: AskForApproval, pub(crate) sandbox_policy: &'a SandboxPolicy, pub(crate) file_system_sandbox_policy: &'a FileSystemSandboxPolicy, pub(crate) sandbox_permissions: SandboxPermissions, pub(crate) prefix_rule: Option>, } impl ExecPolicyManager { pub(crate) fn new(policy: Arc) -> Self { Self { policy: ArcSwap::from(policy), update_lock: tokio::sync::Mutex::new(()), } } #[instrument(level = "info", skip_all)] pub(crate) async fn load(config_stack: &ConfigLayerStack) -> Result { let (policy, warning) = load_exec_policy_with_warning(config_stack).await?; if let Some(err) = warning.as_ref() { tracing::warn!("failed to parse rules: {err}"); } Ok(Self::new(Arc::new(policy))) } pub(crate) fn current(&self) -> Arc { self.policy.load_full() } pub(crate) async fn create_exec_approval_requirement_for_command( &self, req: ExecApprovalRequest<'_>, ) -> ExecApprovalRequirement { let ExecApprovalRequest { command, approval_policy, sandbox_policy, file_system_sandbox_policy, sandbox_permissions, prefix_rule, } = req; let exec_policy = self.current(); let (commands, used_complex_parsing) = commands_for_exec_policy(command); // Keep heredoc prefix parsing for rule evaluation so existing // allow/prompt/forbidden rules still apply, but avoid auto-derived // amendments when only the heredoc fallback parser matched. let auto_amendment_allowed = !used_complex_parsing; let exec_policy_fallback = |cmd: &[String]| { render_decision_for_unmatched_command( approval_policy, sandbox_policy, file_system_sandbox_policy, cmd, sandbox_permissions, used_complex_parsing, ) }; let match_options = MatchOptions { resolve_host_executables: true, }; let evaluation = exec_policy.check_multiple_with_options( commands.iter(), &exec_policy_fallback, &match_options, ); let requested_amendment = derive_requested_execpolicy_amendment_from_prefix_rule( prefix_rule.as_ref(), &evaluation.matched_rules, exec_policy.as_ref(), &commands, &exec_policy_fallback, &match_options, ); match evaluation.decision { Decision::Forbidden => ExecApprovalRequirement::Forbidden { reason: derive_forbidden_reason(command, &evaluation), }, Decision::Prompt => { let prompt_is_rule = evaluation.matched_rules.iter().any(|rule_match| { is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt }); match prompt_is_rejected_by_policy(approval_policy, prompt_is_rule) { Some(reason) => ExecApprovalRequirement::Forbidden { reason: reason.to_string(), }, None => ExecApprovalRequirement::NeedsApproval { reason: derive_prompt_reason(command, &evaluation), proposed_execpolicy_amendment: requested_amendment.or_else(|| { if auto_amendment_allowed { try_derive_execpolicy_amendment_for_prompt_rules( &evaluation.matched_rules, ) } else { None } }), }, } } Decision::Allow => ExecApprovalRequirement::Skip { // Bypass sandbox if execpolicy allows the command bypass_sandbox: evaluation.matched_rules.iter().any(|rule_match| { is_policy_match(rule_match) && rule_match.decision() == Decision::Allow }), proposed_execpolicy_amendment: if auto_amendment_allowed { try_derive_execpolicy_amendment_for_allow_rules(&evaluation.matched_rules) } else { None }, }, } } pub(crate) async fn append_amendment_and_update( &self, codex_home: &Path, amendment: &ExecPolicyAmendment, ) -> Result<(), ExecPolicyUpdateError> { let _update_guard = self.update_lock.lock().await; let policy_path = default_policy_path(codex_home); spawn_blocking({ let policy_path = policy_path.clone(); let prefix = amendment.command.clone(); move || blocking_append_allow_prefix_rule(&policy_path, &prefix) }) .await .map_err(|source| ExecPolicyUpdateError::JoinBlockingTask { source })? .map_err(|source| ExecPolicyUpdateError::AppendRule { path: policy_path, source, })?; let current_policy = self.current(); let match_options = MatchOptions { resolve_host_executables: true, }; let existing_evaluation = current_policy.check_multiple_with_options( [&amendment.command], &|_| Decision::Forbidden, &match_options, ); let already_allowed = existing_evaluation.decision == Decision::Allow && existing_evaluation.matched_rules.iter().any(|rule_match| { is_policy_match(rule_match) && rule_match.decision() == Decision::Allow }); if already_allowed { return Ok(()); } let mut updated_policy = current_policy.as_ref().clone(); updated_policy.add_prefix_rule(&amendment.command, Decision::Allow)?; self.policy.store(Arc::new(updated_policy)); Ok(()) } pub(crate) async fn append_network_rule_and_update( &self, codex_home: &Path, host: &str, protocol: NetworkRuleProtocol, decision: Decision, justification: Option, ) -> Result<(), ExecPolicyUpdateError> { let _update_guard = self.update_lock.lock().await; let policy_path = default_policy_path(codex_home); let host = host.to_string(); spawn_blocking({ let policy_path = policy_path.clone(); let host = host.clone(); let justification = justification.clone(); move || { blocking_append_network_rule( &policy_path, &host, protocol, decision, justification.as_deref(), ) } }) .await .map_err(|source| ExecPolicyUpdateError::JoinBlockingTask { source })? .map_err(|source| ExecPolicyUpdateError::AppendRule { path: policy_path, source, })?; let mut updated_policy = self.current().as_ref().clone(); updated_policy.add_network_rule(&host, protocol, decision, justification)?; self.policy.store(Arc::new(updated_policy)); Ok(()) } } impl Default for ExecPolicyManager { fn default() -> Self { Self::new(Arc::new(Policy::empty())) } } pub async fn check_execpolicy_for_warnings( config_stack: &ConfigLayerStack, ) -> Result, ExecPolicyError> { let (_, warning) = load_exec_policy_with_warning(config_stack).await?; Ok(warning) } fn exec_policy_message_for_display(source: &codex_execpolicy::Error) -> String { let message = source.to_string(); if let Some(line) = message .lines() .find(|line| line.trim_start().starts_with("error: ")) { return line.to_owned(); } if let Some(first_line) = message.lines().next() && let Some((_, detail)) = first_line.rsplit_once(": starlark error: ") { return detail.trim().to_string(); } message .lines() .next() .unwrap_or_default() .trim() .to_string() } fn parse_starlark_line_from_message(message: &str) -> Option<(PathBuf, usize)> { let first_line = message.lines().next()?.trim(); let (path_and_position, _) = first_line.rsplit_once(": starlark error:")?; let mut parts = path_and_position.rsplitn(3, ':'); let _column = parts.next()?.parse::().ok()?; let line = parts.next()?.parse::().ok()?; let path = PathBuf::from(parts.next()?); if line == 0 { return None; } Some((path, line)) } pub fn format_exec_policy_error_with_source(error: &ExecPolicyError) -> String { match error { ExecPolicyError::ParsePolicy { path, source } => { let rendered_source = source.to_string(); let structured_location = source .location() .map(|location| (PathBuf::from(location.path), location.range.start.line)); let parsed_location = parse_starlark_line_from_message(&rendered_source); let location = match (structured_location, parsed_location) { (Some((_, 1)), Some((parsed_path, parsed_line))) if parsed_line > 1 => { Some((parsed_path, parsed_line)) } (Some(structured), _) => Some(structured), (None, parsed) => parsed, }; let message = exec_policy_message_for_display(source); match location { Some((path, line)) => { format!( "{}:{}: {} (problem is on or around line {})", path.display(), line, message, line ) } None => format!("{path}: {message}"), } } _ => error.to_string(), } } async fn load_exec_policy_with_warning( config_stack: &ConfigLayerStack, ) -> Result<(Policy, Option), ExecPolicyError> { match load_exec_policy(config_stack).await { Ok(policy) => Ok((policy, None)), Err(err @ ExecPolicyError::ParsePolicy { .. }) => Ok((Policy::empty(), Some(err))), Err(err) => Err(err), } } pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result { // Iterate the layers in increasing order of precedence, adding the *.rules // from each layer, so that higher-precedence layers can override // rules defined in lower-precedence ones. let mut policy_paths = Vec::new(); for layer in config_stack.get_layers( ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ false, ) { if let Some(config_folder) = layer.config_folder() { #[expect(clippy::expect_used)] let policy_dir = config_folder.join(RULES_DIR_NAME).expect("safe join"); let layer_policy_paths = collect_policy_files(&policy_dir).await?; policy_paths.extend(layer_policy_paths); } } tracing::trace!( policy_paths = ?policy_paths, "loaded exec policies" ); let mut parser = PolicyParser::new(); for policy_path in &policy_paths { let contents = fs::read_to_string(policy_path) .await .map_err(|source| ExecPolicyError::ReadFile { path: policy_path.clone(), source, })?; let identifier = policy_path.to_string_lossy().to_string(); parser .parse(&identifier, &contents) .map_err(|source| ExecPolicyError::ParsePolicy { path: identifier, source, })?; } let policy = parser.build(); tracing::debug!("loaded rules from {} files", policy_paths.len()); tracing::trace!(rules = ?policy, "exec policy rules loaded"); let Some(requirements_policy) = config_stack.requirements().exec_policy.as_deref() else { return Ok(policy); }; Ok(policy.merge_overlay(requirements_policy.as_ref())) } /// If a command is not matched by any execpolicy rule, derive a [`Decision`]. pub fn render_decision_for_unmatched_command( approval_policy: AskForApproval, sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, command: &[String], sandbox_permissions: SandboxPermissions, used_complex_parsing: bool, ) -> Decision { if is_known_safe_command(command) && !used_complex_parsing { return Decision::Allow; } // On Windows, ReadOnly sandbox is not a real sandbox, so special-case it // here. let environment_lacks_sandbox_protections = cfg!(windows) && matches!(sandbox_policy, SandboxPolicy::ReadOnly { .. }); // If the command is flagged as dangerous or we have no sandbox protection, // we should never allow it to run without approval. // // We prefer to prompt the user rather than outright forbid the command, // but if the user has explicitly disabled prompts, we must // forbid the command. if command_might_be_dangerous(command) || environment_lacks_sandbox_protections { return match approval_policy { AskForApproval::Never => { let sandbox_is_explicitly_disabled = matches!( sandbox_policy, SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } ); if sandbox_is_explicitly_disabled { // If the sandbox is explicitly disabled, we should allow the command to run Decision::Allow } else { Decision::Forbidden } } AskForApproval::OnFailure | AskForApproval::OnRequest | AskForApproval::UnlessTrusted | AskForApproval::Granular(_) => Decision::Prompt, }; } match approval_policy { AskForApproval::Never | AskForApproval::OnFailure => { // We allow the command to run, relying on the sandbox for // protection. Decision::Allow } AskForApproval::UnlessTrusted => { // We already checked `is_known_safe_command(command)` and it // returned false, so we must prompt. Decision::Prompt } AskForApproval::OnRequest => { match file_system_sandbox_policy.kind { FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { // The user has indicated we should "just run" commands // in their unrestricted environment, so we do so since the // command has not been flagged as dangerous. Decision::Allow } FileSystemSandboxKind::Restricted => { // In restricted sandboxes, do not prompt for non-escalated, // non-dangerous commands; let the sandbox enforce // restrictions without a user prompt. if sandbox_permissions.requests_sandbox_override() { Decision::Prompt } else { Decision::Allow } } } } AskForApproval::Granular(_) => match file_system_sandbox_policy.kind { FileSystemSandboxKind::Unrestricted | FileSystemSandboxKind::ExternalSandbox => { // Mirror on-request behavior for unmatched commands; prompt-vs-reject is handled // by `prompt_is_rejected_by_policy`. Decision::Allow } FileSystemSandboxKind::Restricted => { if sandbox_permissions.requests_sandbox_override() { Decision::Prompt } else { Decision::Allow } } }, } } fn default_policy_path(codex_home: &Path) -> PathBuf { codex_home.join(RULES_DIR_NAME).join(DEFAULT_POLICY_FILE) } fn commands_for_exec_policy(command: &[String]) -> (Vec>, bool) { if let Some(commands) = parse_shell_lc_plain_commands(command) && !commands.is_empty() { return (commands, false); } if let Some(single_command) = parse_shell_lc_single_command_prefix(command) { return (vec![single_command], true); } (vec![command.to_vec()], false) } /// Derive a proposed execpolicy amendment when a command requires user approval /// - If any execpolicy rule prompts, return None, because an amendment would not skip that policy requirement. /// - Otherwise return the first heuristics Prompt. /// - Examples: /// - execpolicy: empty. Command: `["python"]`. Heuristics prompt -> `Some(vec!["python"])`. /// - execpolicy: empty. Command: `["bash", "-c", "cd /some/folder && prog1 --option1 arg1 && prog2 --option2 arg2"]`. /// Parsed commands include `cd /some/folder`, `prog1 --option1 arg1`, and `prog2 --option2 arg2`. If heuristics allow `cd` but prompt /// on `prog1`, we return `Some(vec!["prog1", "--option1", "arg1"])`. /// - execpolicy: contains a `prompt for prefix ["prog2"]` rule. For the same command as above, /// we return `None` because an execpolicy prompt still applies even if we amend execpolicy to allow ["prog1", "--option1", "arg1"]. fn try_derive_execpolicy_amendment_for_prompt_rules( matched_rules: &[RuleMatch], ) -> Option { if matched_rules .iter() .any(|rule_match| is_policy_match(rule_match) && rule_match.decision() == Decision::Prompt) { return None; } matched_rules .iter() .find_map(|rule_match| match rule_match { RuleMatch::HeuristicsRuleMatch { command, decision: Decision::Prompt, } => Some(ExecPolicyAmendment::from(command.clone())), _ => None, }) } /// - Note: we only use this amendment when the command fails to run in sandbox and codex prompts the user to run outside the sandbox /// - The purpose of this amendment is to bypass sandbox for similar commands in the future /// - If any execpolicy rule matches, return None, because we would already be running command outside the sandbox fn try_derive_execpolicy_amendment_for_allow_rules( matched_rules: &[RuleMatch], ) -> Option { if matched_rules.iter().any(is_policy_match) { return None; } matched_rules .iter() .find_map(|rule_match| match rule_match { RuleMatch::HeuristicsRuleMatch { command, decision: Decision::Allow, } => Some(ExecPolicyAmendment::from(command.clone())), _ => None, }) } fn derive_requested_execpolicy_amendment_from_prefix_rule( prefix_rule: Option<&Vec>, matched_rules: &[RuleMatch], exec_policy: &Policy, commands: &[Vec], exec_policy_fallback: &impl Fn(&[String]) -> Decision, match_options: &MatchOptions, ) -> Option { let prefix_rule = prefix_rule?; if prefix_rule.is_empty() { return None; } if BANNED_PREFIX_SUGGESTIONS.iter().any(|banned| { prefix_rule.len() == banned.len() && prefix_rule .iter() .map(String::as_str) .eq(banned.iter().copied()) }) { return None; } // if any policy rule already matches, don't suggest an additional rule that might conflict or not apply if matched_rules.iter().any(is_policy_match) { return None; } let amendment = ExecPolicyAmendment::new(prefix_rule.clone()); if prefix_rule_would_approve_all_commands( exec_policy, &amendment.command, commands, exec_policy_fallback, match_options, ) { Some(amendment) } else { None } } fn prefix_rule_would_approve_all_commands( exec_policy: &Policy, prefix_rule: &[String], commands: &[Vec], exec_policy_fallback: &impl Fn(&[String]) -> Decision, match_options: &MatchOptions, ) -> bool { let mut policy_with_prefix_rule = exec_policy.clone(); if policy_with_prefix_rule .add_prefix_rule(prefix_rule, Decision::Allow) .is_err() { return false; } commands.iter().all(|command| { policy_with_prefix_rule .check_with_options(command, exec_policy_fallback, match_options) .decision == Decision::Allow }) } /// Only return a reason when a policy rule drove the prompt decision. fn derive_prompt_reason(command_args: &[String], evaluation: &Evaluation) -> Option { let command = render_shlex_command(command_args); let most_specific_prompt = evaluation .matched_rules .iter() .filter_map(|rule_match| match rule_match { RuleMatch::PrefixRuleMatch { matched_prefix, decision: Decision::Prompt, justification, .. } => Some((matched_prefix.len(), justification.as_deref())), _ => None, }) .max_by_key(|(matched_prefix_len, _)| *matched_prefix_len); match most_specific_prompt { Some((_matched_prefix_len, Some(justification))) => { Some(format!("`{command}` requires approval: {justification}")) } Some((_matched_prefix_len, None)) => { Some(format!("`{command}` requires approval by policy")) } None => None, } } fn render_shlex_command(args: &[String]) -> String { shlex_try_join(args.iter().map(String::as_str)).unwrap_or_else(|_| args.join(" ")) } /// Derive a string explaining why the command was forbidden. If `justification` /// is set by the user, this can contain instructions with recommended /// alternatives, for example. fn derive_forbidden_reason(command_args: &[String], evaluation: &Evaluation) -> String { let command = render_shlex_command(command_args); let most_specific_forbidden = evaluation .matched_rules .iter() .filter_map(|rule_match| match rule_match { RuleMatch::PrefixRuleMatch { matched_prefix, decision: Decision::Forbidden, justification, .. } => Some((matched_prefix, justification.as_deref())), _ => None, }) .max_by_key(|(matched_prefix, _)| matched_prefix.len()); match most_specific_forbidden { Some((_matched_prefix, Some(justification))) => { format!("`{command}` rejected: {justification}") } Some((matched_prefix, None)) => { let prefix = render_shlex_command(matched_prefix); format!("`{command}` rejected: policy forbids commands starting with `{prefix}`") } None => format!("`{command}` rejected: blocked by policy"), } } async fn collect_policy_files(dir: impl AsRef) -> Result, ExecPolicyError> { let dir = dir.as_ref(); let mut read_dir = match fs::read_dir(dir).await { Ok(read_dir) => read_dir, Err(err) if err.kind() == ErrorKind::NotFound => return Ok(Vec::new()), Err(source) => { return Err(ExecPolicyError::ReadDir { dir: dir.to_path_buf(), source, }); } }; let mut policy_paths = Vec::new(); while let Some(entry) = read_dir .next_entry() .await .map_err(|source| ExecPolicyError::ReadDir { dir: dir.to_path_buf(), source, })? { let path = entry.path(); let file_type = entry .file_type() .await .map_err(|source| ExecPolicyError::ReadDir { dir: dir.to_path_buf(), source, })?; if path .extension() .and_then(|ext| ext.to_str()) .is_some_and(|ext| ext == RULE_EXTENSION) && file_type.is_file() { policy_paths.push(path); } } policy_paths.sort(); tracing::debug!( "loaded {} .rules files in {}", policy_paths.len(), dir.display() ); Ok(policy_paths) } #[cfg(test)] #[path = "exec_policy_tests.rs"] mod tests;