use std::sync::{Arc, Mutex};
use crate::core::error::ShellError;
use super::ShellSecurity;
use super::enforcement::{enforce_shell_allowlist, enforce_with, refuse_redaction_marker};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalMiss {
pub program: String,
pub once_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalRequest {
pub misses: Vec<ApprovalMiss>,
}
impl ApprovalRequest {
#[must_use]
pub fn programs(&self) -> Vec<String> {
self.misses.iter().map(|m| m.program.clone()).collect()
}
#[must_use]
pub fn allows_lasting_approval(&self) -> bool {
!self.misses.iter().any(|m| m.once_only)
}
}
#[must_use]
pub fn approval_request(command: &str) -> Option<ApprovalRequest> {
if refuse_redaction_marker(command).is_err()
|| ShellSecurity::resolve() != ShellSecurity::Enforce
|| enforce_shell_allowlist(command).is_ok()
{
return None;
}
approval_request_enforced(command)
}
pub(super) fn approval_request_enforced(command: &str) -> Option<ApprovalRequest> {
let log = Arc::new(Mutex::new(Vec::new()));
enforce_with(command, &[], Some(&log)).ok()?;
let misses: Vec<ApprovalMiss> = std::mem::take(
&mut *log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
if misses.is_empty() || !misses.iter().all(|m| is_approvable_name(&m.program)) {
return None;
}
Some(ApprovalRequest { misses })
}
fn is_approvable_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 64
&& !name.starts_with('-')
&& !name.bytes().all(|b| b == b'.')
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'+' | b'-'))
}
pub fn check_shell_allowlist_granted(command: &str, granted: &[String]) -> Result<(), ShellError> {
if granted.is_empty() {
return super::check_shell_allowlist(command);
}
refuse_redaction_marker(command)?;
match ShellSecurity::resolve() {
ShellSecurity::Off | ShellSecurity::Warn => super::check_shell_allowlist(command),
ShellSecurity::Enforce => enforce_with(command, granted, None).inspect_err(|_| {
crate::core::security_events::note(
crate::core::security_events::SecurityKind::ShellBlocked,
1,
);
}),
}
}
pub fn allow_programs(programs: &[String]) -> Result<Vec<String>, String> {
crate::core::config::setter::add_to_global_list("shell_allowlist_extra", programs)
}
pub fn allow_execute_languages(languages: &[String]) -> Result<Vec<String>, String> {
crate::core::config::setter::add_to_global_list("execute_allowed_languages", languages)
}
#[cfg(test)]
#[path = "approval_tests.rs"]
mod tests;