lean_ctx/core/shell_allowlist/
mod.rs1#[doc(hidden)]
12pub mod case_construct;
13#[doc(hidden)]
14pub mod compound;
15#[doc(hidden)]
16pub mod heredoc;
17#[doc(hidden)]
18pub mod lexer;
19#[doc(hidden)]
20pub mod mode;
21#[doc(hidden)]
22pub mod powershell;
23#[doc(hidden)]
24pub mod ps_statements;
25#[doc(hidden)]
26pub mod tokenizer;
27
28use std::sync::OnceLock;
29
30use crate::core::error::ShellError;
31
32pub use case_construct::{contains_double_semicolon, find_shell_word, rewrite_case_constructs};
33pub use compound::expand_to_leaf_segments;
34pub use heredoc::{
35 heredoc_delims, read_heredoc_delim, strip_all_heredoc_bodies, strip_for_analysis,
36};
37pub use mode::ShellSecurity;
38pub use ps_statements::{Dialect, with_dialect};
39pub use tokenizer::*;
40
41pub const MAX_VALIDATED_COMMAND_BYTES: usize = 64 * 1024;
45
46#[derive(Clone, Copy)]
48pub struct ShellGate {
49 pub check: fn(&str) -> Result<(), ShellError>,
51 pub warn_only_reason: fn(&str) -> String,
53}
54
55static GATE: OnceLock<ShellGate> = OnceLock::new();
56
57#[must_use]
61pub fn register_gate(gate: ShellGate) -> bool {
62 GATE.set(gate).is_ok()
63}
64
65#[cfg(any(test, feature = "test-support"))]
71#[doc(hidden)]
72pub fn install_test_gate() {
73 fn check(command: &str) -> Result<(), ShellError> {
74 let Ok(list) = std::env::var("LEAN_CTX_SHELL_ALLOWLIST_OVERRIDE") else {
75 return Ok(());
76 };
77 let first = command.split_whitespace().next().unwrap_or_default();
78 if list.split(',').any(|entry| entry.trim() == first) {
79 Ok(())
80 } else {
81 Err(ShellError::from(format!(
82 "test gate: '{first}' is not in the override allowlist"
83 )))
84 }
85 }
86 let _ = GATE.set(ShellGate {
87 check,
88 warn_only_reason: str::to_string,
89 });
90}
91
92pub fn gate_registered() -> bool {
94 GATE.get().is_some()
95}
96
97pub fn check_registered(command: &str) -> Result<(), ShellError> {
101 match GATE.get() {
102 Some(gate) => (gate.check)(command),
103 None => Err(ShellError::from(
104 "lean-ctx: no shell-allowlist policy is registered in this build; \
105 refusing to run the command (fail closed)",
106 )),
107 }
108}
109
110pub fn warn_only_reason_registered(message: &str) -> String {
113 match GATE.get() {
114 Some(gate) => (gate.warn_only_reason)(message),
115 None => message.to_string(),
116 }
117}