mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Record normalized sandbox violation events (#36207)
## Why Filesystem denials and managed-network blocks did not share a structured event shape, requiring downstream consumers to rediscover enforcement paths and parse backend-specific output. See https://github.com/openai/codex/pull/17573. ## What changed - Add normalized filesystem and network violation types in `codex-sandboxing` and emit them through a shared tracing seam. - Classify filesystem denials by backend and reason, retaining an optional path and bounded output snippet, and preserve managed-network block context. - Report the sandbox type through exec-server responses so unified exec can classify remote denials without guessing; omitted values remain compatible with older peers. - Record violations from exec, apply-patch, shell-escalation, unified-exec, and managed-network enforcement paths without changing denial behavior. ## Testing - Cover filesystem classification, path extraction, `SIGSYS`, network event conversion, protocol compatibility, and remote sandbox-type propagation. GitOrigin-RevId: d673173b4fa6bdf0a24421194a8a61c81fab9c96
This commit is contained in:
@@ -44,6 +44,7 @@ use codex_sandboxing::WindowsSandboxFilesystemOverrides;
|
||||
pub(crate) use codex_sandboxing::is_likely_sandbox_denied;
|
||||
#[cfg(test)]
|
||||
use codex_sandboxing::permission_profile_supports_windows_restricted_token_sandbox;
|
||||
use codex_sandboxing::record_filesystem_sandbox_violation;
|
||||
use codex_sandboxing::resolve_windows_elevated_filesystem_overrides;
|
||||
use codex_sandboxing::resolve_windows_restricted_token_filesystem_overrides;
|
||||
#[cfg(test)]
|
||||
@@ -814,6 +815,7 @@ fn finalize_exec_result(
|
||||
}
|
||||
|
||||
if is_likely_sandbox_denied(sandbox_type, &exec_output) {
|
||||
record_filesystem_sandbox_violation(sandbox_type, &exec_output);
|
||||
return Err(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(exec_output),
|
||||
network_policy_decision: None,
|
||||
|
||||
@@ -1112,11 +1112,9 @@ impl Session {
|
||||
current_exec_policy.as_ref(),
|
||||
&session_configuration.permission_profile(),
|
||||
/*network_policy_decider*/ None,
|
||||
self.services
|
||||
.managed_network_requirements_configured
|
||||
.then(|| {
|
||||
build_blocked_request_observer(Arc::clone(&self.services.network_approval))
|
||||
}),
|
||||
Some(build_blocked_request_observer(Arc::clone(
|
||||
&self.services.network_approval,
|
||||
))),
|
||||
self.services.managed_network_requirements_configured,
|
||||
self.services.network_proxy_audit_metadata.clone(),
|
||||
)
|
||||
|
||||
@@ -996,15 +996,11 @@ impl Session {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let blocked_request_observer = if managed_network_requirements_configured {
|
||||
config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.map(|_| build_blocked_request_observer(Arc::clone(&network_approval)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let blocked_request_observer = config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.map(|_| build_blocked_request_observer(Arc::clone(&network_approval)));
|
||||
let network_policy_decider =
|
||||
network_policy_decider_session
|
||||
.as_ref()
|
||||
|
||||
@@ -28,6 +28,7 @@ use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_sandboxing::record_network_sandbox_violation;
|
||||
use indexmap::IndexMap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -1047,6 +1048,7 @@ pub(crate) fn build_blocked_request_observer(
|
||||
Arc::new(move |blocked: BlockedRequest| {
|
||||
let network_approval = Arc::clone(&network_approval);
|
||||
async move {
|
||||
record_network_sandbox_violation(&blocked);
|
||||
network_approval.record_blocked_request(blocked).await;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_sandboxing::policy_transforms::effective_permission_profile;
|
||||
use codex_sandboxing::record_filesystem_sandbox_violation;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use futures::future::BoxFuture;
|
||||
use std::path::PathBuf;
|
||||
@@ -259,6 +260,7 @@ impl ToolRuntime<ApplyPatchRequest, ApplyPatchRuntimeOutput> for ApplyPatchRunti
|
||||
timed_out: false,
|
||||
};
|
||||
if failed && is_likely_sandbox_denied(attempt.sandbox, &output) {
|
||||
record_filesystem_sandbox_violation(attempt.sandbox, &output);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(output),
|
||||
network_policy_decision: None,
|
||||
|
||||
@@ -45,6 +45,7 @@ use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_sandboxing::record_filesystem_sandbox_violation;
|
||||
use codex_shell_command::bash::parse_shell_lc_plain_commands;
|
||||
use codex_shell_command::bash::parse_shell_lc_single_command_prefix;
|
||||
use codex_shell_escalation::EscalateServer;
|
||||
@@ -1061,6 +1062,7 @@ fn map_exec_result(
|
||||
}
|
||||
|
||||
if is_likely_sandbox_denied(sandbox, &output) {
|
||||
record_filesystem_sandbox_violation(sandbox, &output);
|
||||
return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(output),
|
||||
network_policy_decision: None,
|
||||
|
||||
@@ -304,6 +304,7 @@ async fn blocking_terminate_unified_process(
|
||||
allow_terminate,
|
||||
wake_tx,
|
||||
}),
|
||||
sandbox_type: Some(codex_sandboxing::SandboxType::None),
|
||||
})
|
||||
.await?,
|
||||
))
|
||||
|
||||
@@ -12,7 +12,6 @@ use tokio::task::JoinHandle;
|
||||
use tokio::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::exec::is_likely_sandbox_denied;
|
||||
use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ExecProcessEvent;
|
||||
use codex_exec_server::ProcessSignal as ExecServerProcessSignal;
|
||||
@@ -23,6 +22,8 @@ use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use codex_protocol::protocol::TruncationPolicy;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::is_likely_sandbox_denied;
|
||||
use codex_sandboxing::record_filesystem_sandbox_violation;
|
||||
use codex_utils_output_truncation::formatted_truncate_text;
|
||||
use codex_utils_pty::ExecCommandSession;
|
||||
use codex_utils_pty::ProcessSignal as PtyProcessSignal;
|
||||
@@ -315,7 +316,11 @@ impl UnifiedExecProcess {
|
||||
aggregated_output: StreamOutput::new(text.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
if executor_reported_denial || is_likely_sandbox_denied(sandbox_type, &exec_output) {
|
||||
let likely_sandbox_denial = is_likely_sandbox_denied(sandbox_type, &exec_output);
|
||||
if likely_sandbox_denial {
|
||||
record_filesystem_sandbox_violation(sandbox_type, &exec_output);
|
||||
}
|
||||
if executor_reported_denial || likely_sandbox_denial {
|
||||
let snippet = formatted_truncate_text(
|
||||
text,
|
||||
TruncationPolicy::Tokens(UNIFIED_EXEC_OUTPUT_MAX_TOKENS),
|
||||
@@ -394,11 +399,10 @@ impl UnifiedExecProcess {
|
||||
started: StartedExecProcess,
|
||||
) -> Result<Self, UnifiedExecError> {
|
||||
let process_handle = ProcessHandle::ExecServer(Arc::clone(&started.process));
|
||||
let mut managed = Self::new(
|
||||
process_handle,
|
||||
SandboxType::None,
|
||||
/*spawn_lifecycle*/ None,
|
||||
);
|
||||
// Older peers do not report this field. In that case, skip local
|
||||
// classification rather than attributing a violation to a guessed backend.
|
||||
let sandbox_type = started.sandbox_type.unwrap_or(SandboxType::None);
|
||||
let mut managed = Self::new(process_handle, sandbox_type, /*spawn_lifecycle*/ None);
|
||||
let output_handles = managed.output_handles();
|
||||
managed.output_task = Some(Self::spawn_exec_server_output_task(
|
||||
started,
|
||||
|
||||
@@ -512,6 +512,7 @@ async fn pruning_does_not_evict_live_process_while_exited_process_is_finalizing(
|
||||
crate::unified_exec::process_tests::remote_process(
|
||||
codex_exec_server::WriteStatus::Accepted,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -523,6 +524,7 @@ async fn pruning_does_not_evict_live_process_while_exited_process_is_finalizing(
|
||||
crate::unified_exec::process_tests::remote_process(
|
||||
codex_exec_server::WriteStatus::Accepted,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
|
||||
@@ -88,6 +88,7 @@ impl ExecProcess for MockExecProcess {
|
||||
pub(super) async fn remote_process(
|
||||
write_status: WriteStatus,
|
||||
terminate_error: Option<String>,
|
||||
sandbox_type: codex_sandboxing::SandboxType,
|
||||
) -> UnifiedExecProcess {
|
||||
let (wake_tx, _wake_rx) = watch::channel(0);
|
||||
let started = StartedExecProcess {
|
||||
@@ -100,6 +101,7 @@ pub(super) async fn remote_process(
|
||||
terminate_error,
|
||||
wake_tx,
|
||||
}),
|
||||
sandbox_type: Some(sandbox_type),
|
||||
};
|
||||
|
||||
UnifiedExecProcess::from_exec_server_started(started)
|
||||
@@ -109,7 +111,12 @@ pub(super) async fn remote_process(
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_write_unknown_process_marks_process_exited() {
|
||||
let process = remote_process(WriteStatus::UnknownProcess, /*terminate_error*/ None).await;
|
||||
let process = remote_process(
|
||||
WriteStatus::UnknownProcess,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let err = process
|
||||
.write(b"hello")
|
||||
@@ -122,7 +129,12 @@ async fn remote_write_unknown_process_marks_process_exited() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_write_closed_stdin_marks_process_exited() {
|
||||
let process = remote_process(WriteStatus::StdinClosed, /*terminate_error*/ None).await;
|
||||
let process = remote_process(
|
||||
WriteStatus::StdinClosed,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let err = process
|
||||
.write(b"hello")
|
||||
@@ -135,7 +147,12 @@ async fn remote_write_closed_stdin_marks_process_exited() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_and_terminate_preserves_failure_message() {
|
||||
let process = remote_process(WriteStatus::Accepted, /*terminate_error*/ None).await;
|
||||
let process = remote_process(
|
||||
WriteStatus::Accepted,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
process.fail_and_terminate("network denied".to_string());
|
||||
process.fail_and_terminate("second failure".to_string());
|
||||
@@ -152,6 +169,7 @@ async fn remote_terminate_confirmed_updates_state_on_success_only() {
|
||||
let process = remote_process(
|
||||
WriteStatus::Accepted,
|
||||
Some("terminate unavailable".to_string()),
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -163,7 +181,12 @@ async fn remote_terminate_confirmed_updates_state_on_success_only() {
|
||||
assert!(matches!(err, UnifiedExecError::ProcessFailed { .. }));
|
||||
assert!(!process.has_exited());
|
||||
|
||||
let process = remote_process(WriteStatus::Accepted, /*terminate_error*/ None).await;
|
||||
let process = remote_process(
|
||||
WriteStatus::Accepted,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
process
|
||||
.terminate_confirmed()
|
||||
@@ -172,3 +195,18 @@ async fn remote_terminate_confirmed_updates_state_on_success_only() {
|
||||
|
||||
assert!(process.has_exited());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_process_preserves_executor_sandbox_type() {
|
||||
let process = remote_process(
|
||||
WriteStatus::Accepted,
|
||||
/*terminate_error*/ None,
|
||||
codex_sandboxing::SandboxType::LinuxSeccomp,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
process.sandbox_type(),
|
||||
codex_sandboxing::SandboxType::LinuxSeccomp
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,6 +215,20 @@ pub struct ExecEnvPolicy {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecResponse {
|
||||
pub process_id: ProcessId,
|
||||
/// `None` means the peer did not report its sandbox type. Current peers
|
||||
/// report [`ProcessSandboxType::None`] when the process was not sandboxed.
|
||||
#[serde(default)]
|
||||
pub sandbox_type: Option<ProcessSandboxType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ProcessSandboxType {
|
||||
/// The process was explicitly started without a platform sandbox.
|
||||
None,
|
||||
MacosSeatbelt,
|
||||
LinuxSeccomp,
|
||||
WindowsRestrictedToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -747,9 +761,11 @@ mod tests {
|
||||
use super::EnvironmentInfo;
|
||||
use super::ExecExitedNotification;
|
||||
use super::ExecParams;
|
||||
use super::ExecResponse;
|
||||
use super::FsReadFileParams;
|
||||
use super::HttpRequestParams;
|
||||
use super::ProcessId;
|
||||
use super::ProcessSandboxType;
|
||||
use super::ShellInfo;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
@@ -1050,4 +1066,22 @@ mod tests {
|
||||
|
||||
assert_eq!(notification.sandbox_denied, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_response_distinguishes_unknown_from_explicitly_unsandboxed() {
|
||||
let unknown: ExecResponse = serde_json::from_value(serde_json::json!({
|
||||
"processId": "legacy",
|
||||
}))
|
||||
.expect("legacy response should deserialize");
|
||||
let unsandboxed: ExecResponse = serde_json::from_value(serde_json::json!({
|
||||
"processId": "current",
|
||||
"sandboxType": "none",
|
||||
}))
|
||||
.expect("explicitly unsandboxed response should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
(unknown.sandbox_type, unsandboxed.sandbox_type),
|
||||
(None, Some(ProcessSandboxType::None))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ use crate::protocol::INITIALIZED_METHOD;
|
||||
use crate::protocol::InitializeParams;
|
||||
use crate::protocol::InitializeResponse;
|
||||
use crate::protocol::ProcessOutputChunk;
|
||||
use crate::protocol::ProcessSandboxType;
|
||||
use crate::protocol::ProcessSignal;
|
||||
use crate::protocol::ReadParams;
|
||||
use crate::protocol::ReadResponse;
|
||||
@@ -211,6 +212,7 @@ struct OrderedSessionEvents {
|
||||
pub(crate) struct Session {
|
||||
client: ExecServerClient,
|
||||
process_id: ProcessId,
|
||||
sandbox_type: Option<ProcessSandboxType>,
|
||||
state: Arc<SessionState>,
|
||||
}
|
||||
|
||||
@@ -936,11 +938,12 @@ impl ExecServerClient {
|
||||
.call_rpc::<_, ExecResponse>(&rpc_client, EXEC_METHOD, ¶ms)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
Ok(response) => {
|
||||
state.recoverable.store(true, Ordering::Release);
|
||||
let session = Session {
|
||||
client: client.clone(),
|
||||
process_id: process_id.clone(),
|
||||
sandbox_type: response.sandbox_type,
|
||||
state: Arc::clone(&state),
|
||||
};
|
||||
// Wait for caller receipt so cancellation after send still triggers cleanup.
|
||||
@@ -990,6 +993,7 @@ impl ExecServerClient {
|
||||
Ok(Session {
|
||||
client: self.clone(),
|
||||
process_id: process_id.clone(),
|
||||
sandbox_type: None,
|
||||
state,
|
||||
})
|
||||
}
|
||||
@@ -1340,6 +1344,10 @@ impl Session {
|
||||
&self.process_id
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox_type(&self) -> Option<ProcessSandboxType> {
|
||||
self.sandbox_type
|
||||
}
|
||||
|
||||
pub(crate) fn subscribe_wake(&self) -> watch::Receiver<u64> {
|
||||
self.state.subscribe()
|
||||
}
|
||||
@@ -1662,6 +1670,7 @@ mod tests {
|
||||
use crate::protocol::INITIALIZED_METHOD;
|
||||
use crate::protocol::InitializeResponse;
|
||||
use crate::protocol::ProcessOutputChunk;
|
||||
use crate::protocol::ProcessSandboxType;
|
||||
use crate::protocol::ReadResponse;
|
||||
use crate::protocol::WriteParams;
|
||||
use crate::protocol::WriteResponse;
|
||||
@@ -1733,6 +1742,7 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(ExecResponse {
|
||||
process_id: params.process_id,
|
||||
sandbox_type: Some(ProcessSandboxType::LinuxSeccomp),
|
||||
})
|
||||
.expect("process start response should serialize"),
|
||||
}),
|
||||
@@ -1790,6 +1800,10 @@ mod tests {
|
||||
.expect("process start should succeed");
|
||||
|
||||
assert_eq!(session.process_id(), &process_id);
|
||||
assert_eq!(
|
||||
session.sandbox_type(),
|
||||
Some(ProcessSandboxType::LinuxSeccomp)
|
||||
);
|
||||
let trace = server.await.expect("server task").expect("trace context");
|
||||
let expected_traceparent = expected_trace
|
||||
.traceparent
|
||||
|
||||
@@ -36,6 +36,7 @@ use crate::ProcessId;
|
||||
use crate::StartedExecProcess;
|
||||
use crate::network_policy_decisions::network_policy_decider;
|
||||
use crate::process::ExecProcessEventLog;
|
||||
use crate::process::sandbox_type_from_protocol;
|
||||
use crate::process_sandbox::prepare_exec_request;
|
||||
use crate::protocol::EXEC_CLOSED_METHOD;
|
||||
use crate::protocol::ExecClosedNotification;
|
||||
@@ -47,6 +48,7 @@ use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::MAX_NETWORK_POLICY_PROCESS_ID_BYTES;
|
||||
use crate::protocol::ProcessOutputChunk;
|
||||
use crate::protocol::ProcessSandboxType;
|
||||
use crate::protocol::ProcessSignal;
|
||||
use crate::protocol::ReadParams;
|
||||
use crate::protocol::ReadResponse;
|
||||
@@ -302,6 +304,12 @@ impl LocalProcess {
|
||||
if prepared.command.is_empty() {
|
||||
return Err(invalid_params("argv must not be empty".to_string()));
|
||||
}
|
||||
let sandbox_type = match prepared.sandbox {
|
||||
SandboxType::None => Some(ProcessSandboxType::None),
|
||||
SandboxType::MacosSeatbelt => Some(ProcessSandboxType::MacosSeatbelt),
|
||||
SandboxType::LinuxSeccomp => Some(ProcessSandboxType::LinuxSeccomp),
|
||||
SandboxType::WindowsRestrictedToken => Some(ProcessSandboxType::WindowsRestrictedToken),
|
||||
};
|
||||
|
||||
let start = Arc::new(ProcessStart);
|
||||
{
|
||||
@@ -417,7 +425,14 @@ impl LocalProcess {
|
||||
output_notify,
|
||||
));
|
||||
|
||||
Ok((ExecResponse { process_id }, wake_tx, events))
|
||||
Ok((
|
||||
ExecResponse {
|
||||
process_id,
|
||||
sandbox_type,
|
||||
},
|
||||
wake_tx,
|
||||
events,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
|
||||
@@ -662,6 +677,7 @@ impl LocalProcess {
|
||||
.start_process(params)
|
||||
.await
|
||||
.map_err(map_handler_error)?;
|
||||
let sandbox_type = sandbox_type_from_protocol(response.sandbox_type);
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(LocalExecProcess {
|
||||
process_id: response.process_id,
|
||||
@@ -669,6 +685,7 @@ impl LocalProcess {
|
||||
wake_tx,
|
||||
events,
|
||||
}),
|
||||
sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -928,6 +945,8 @@ async fn watch_exit(
|
||||
),
|
||||
..Default::default()
|
||||
};
|
||||
// Transport the classification to the caller; recording there
|
||||
// attaches audit context once and avoids duplicate events.
|
||||
process.sandbox_denied = is_likely_sandbox_denied(process.sandbox, &exec_output);
|
||||
}
|
||||
let _ = process.wake_tx.send(seq);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
use std::sync::Mutex as StdMutex;
|
||||
|
||||
use codex_network_proxy::NetworkPolicyDecider;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::sync::watch;
|
||||
|
||||
@@ -12,12 +13,29 @@ use crate::ExecServerError;
|
||||
use crate::ProcessId;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ProcessOutputChunk;
|
||||
use crate::protocol::ProcessSandboxType;
|
||||
use crate::protocol::ProcessSignal;
|
||||
use crate::protocol::ReadResponse;
|
||||
use crate::protocol::WriteResponse;
|
||||
|
||||
pub struct StartedExecProcess {
|
||||
pub process: Arc<dyn ExecProcess>,
|
||||
/// `None` means the exec-server peer did not report its sandbox type.
|
||||
pub sandbox_type: Option<SandboxType>,
|
||||
}
|
||||
|
||||
pub(crate) fn sandbox_type_from_protocol(
|
||||
sandbox_type: Option<ProcessSandboxType>,
|
||||
) -> Option<SandboxType> {
|
||||
match sandbox_type {
|
||||
None => None,
|
||||
Some(ProcessSandboxType::None) => Some(SandboxType::None),
|
||||
Some(ProcessSandboxType::MacosSeatbelt) => Some(SandboxType::MacosSeatbelt),
|
||||
Some(ProcessSandboxType::LinuxSeccomp) => Some(SandboxType::LinuxSeccomp),
|
||||
Some(ProcessSandboxType::WindowsRestrictedToken) => {
|
||||
Some(SandboxType::WindowsRestrictedToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pushed process events for consumers that want to follow process output as it
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::ExecProcessFuture;
|
||||
use crate::StartedExecProcess;
|
||||
use crate::client::LazyRemoteExecServerClient;
|
||||
use crate::client::Session;
|
||||
use crate::process::sandbox_type_from_protocol;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ProcessSignal;
|
||||
use crate::protocol::ReadResponse;
|
||||
@@ -39,9 +40,11 @@ impl RemoteProcess {
|
||||
) -> Result<StartedExecProcess, crate::ExecServerError> {
|
||||
let client = self.client.get().await?;
|
||||
let session = client.start_process(params, network_policy_decider).await?;
|
||||
let sandbox_type = sandbox_type_from_protocol(session.sandbox_type());
|
||||
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(RemoteExecProcess { session }),
|
||||
sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_exec_server::WriteResponse;
|
||||
use codex_exec_server::WriteStatus;
|
||||
use codex_exec_server_protocol::JSONRPCMessage;
|
||||
use codex_exec_server_protocol::JSONRPCResponse;
|
||||
use codex_exec_server_protocol::ProcessSandboxType;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -72,6 +73,7 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
sandbox_type: Some(ProcessSandboxType::None),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -137,6 +139,7 @@ async fn exec_server_defaults_omitted_pipe_stdin_to_closed_stdin() -> anyhow::Re
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-default-stdin"),
|
||||
sandbox_type: Some(ProcessSandboxType::None),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ use codex_exec_server::NoiseRendezvousConnectBundle;
|
||||
use codex_exec_server::NoiseRendezvousConnectProvider;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::RemoteEnvironmentConfig;
|
||||
use codex_exec_server_protocol::ProcessSandboxType;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_http_client::cache_system_proxy_route_for_test;
|
||||
@@ -414,6 +415,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
|
||||
response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
sandbox_type: Some(ProcessSandboxType::None),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,14 @@ use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
|
||||
use crate::SandboxType;
|
||||
|
||||
/// Returns whether a failed command was likely denied by the selected sandbox.
|
||||
/// We don't have a fully deterministic way to tell if our command failed
|
||||
/// because of the sandbox - a command in the user's zshrc file might hit an
|
||||
/// error, but the command itself might fail or succeed for other reasons.
|
||||
/// For now, we conservatively check for well known command failure exit codes and
|
||||
/// also look for common sandbox denial keywords in the command output.
|
||||
///
|
||||
/// This predicate is intentionally side-effect free. Callers that handle a
|
||||
/// denial should record it where the relevant audit context is available.
|
||||
pub fn is_likely_sandbox_denied(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
@@ -11,6 +18,10 @@ pub fn is_likely_sandbox_denied(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Quick rejects: well-known non-sandbox shell exit codes
|
||||
// 2: misuse of shell builtins
|
||||
// 126: permission denied
|
||||
// 127: command not found
|
||||
const SANDBOX_DENIED_KEYWORDS: [&str; 7] = [
|
||||
"operation not permitted",
|
||||
"permission denied",
|
||||
@@ -46,8 +57,9 @@ pub fn is_likely_sandbox_denied(
|
||||
#[cfg(unix)]
|
||||
{
|
||||
const EXIT_CODE_SIGNAL_BASE: i32 = 128;
|
||||
const SIGSYS_CODE: i32 = libc::SIGSYS;
|
||||
if sandbox_type == SandboxType::LinuxSeccomp
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + SIGSYS_CODE
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod policy_transforms;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod seatbelt;
|
||||
mod spawn;
|
||||
mod violation;
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -29,6 +30,14 @@ pub use manager::with_managed_mitm_ca_readable_root;
|
||||
pub use spawn::SpawnRequest;
|
||||
pub use spawn::WindowsSandboxSpawnRequest;
|
||||
pub use spawn::spawn_process;
|
||||
pub use violation::FileSystemSandboxViolation;
|
||||
pub use violation::FileSystemSandboxViolationReason;
|
||||
pub use violation::NetworkSandboxViolation;
|
||||
pub use violation::SandboxViolationBackend;
|
||||
pub use violation::SandboxViolationEvent;
|
||||
pub use violation::record_filesystem_sandbox_violation;
|
||||
pub use violation::record_network_sandbox_violation;
|
||||
pub use violation::record_sandbox_violation;
|
||||
pub use windows::WindowsSandboxFilesystemOverrides;
|
||||
pub use windows::permission_profile_supports_windows_restricted_token_sandbox;
|
||||
pub use windows::resolve_windows_elevated_filesystem_overrides;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
use crate::SandboxType;
|
||||
use codex_network_proxy::BlockedRequest;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use tracing::warn;
|
||||
|
||||
#[cfg(unix)]
|
||||
const EXIT_CODE_SIGNAL_BASE: i32 = 128;
|
||||
const OUTPUT_SNIPPET_MAX_CHARS: usize = 512;
|
||||
|
||||
const SANDBOX_DENIED_KEYWORDS: [(FileSystemSandboxViolationReason, &str); 7] = [
|
||||
(
|
||||
FileSystemSandboxViolationReason::OperationNotPermitted,
|
||||
"operation not permitted",
|
||||
),
|
||||
(
|
||||
FileSystemSandboxViolationReason::PermissionDenied,
|
||||
"permission denied",
|
||||
),
|
||||
(
|
||||
FileSystemSandboxViolationReason::ReadOnlyFileSystem,
|
||||
"read-only file system",
|
||||
),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "seccomp"),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "sandbox"),
|
||||
(FileSystemSandboxViolationReason::PolicyDenied, "landlock"),
|
||||
(
|
||||
FileSystemSandboxViolationReason::FailedToWriteFile,
|
||||
"failed to write file",
|
||||
),
|
||||
];
|
||||
|
||||
// Quick rejects: well-known non-sandbox shell exit codes.
|
||||
// 2: misuse of shell builtins
|
||||
// 126: permission denied
|
||||
// 127: command not found
|
||||
const QUICK_REJECT_EXIT_CODES: [i32; 3] = [2, 126, 127];
|
||||
|
||||
/// A normalized sandbox violation observed by Codex sandbox enforcement.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum SandboxViolationEvent {
|
||||
FileSystem(FileSystemSandboxViolation),
|
||||
Network(NetworkSandboxViolation),
|
||||
}
|
||||
|
||||
/// Enforcement backend that observed a sandbox violation.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SandboxViolationBackend {
|
||||
LinuxSandbox,
|
||||
ManagedNetworkProxy,
|
||||
Seatbelt,
|
||||
WindowsSandbox,
|
||||
}
|
||||
|
||||
impl SandboxViolationBackend {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::LinuxSandbox => "linux_sandbox",
|
||||
Self::ManagedNetworkProxy => "managed_network_proxy",
|
||||
Self::Seatbelt => "seatbelt",
|
||||
Self::WindowsSandbox => "windows_sandbox",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A filesystem sandbox denial inferred from a sandboxed process result.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FileSystemSandboxViolation {
|
||||
pub backend: SandboxViolationBackend,
|
||||
pub reason: FileSystemSandboxViolationReason,
|
||||
pub path: Option<String>,
|
||||
pub output_snippet: String,
|
||||
}
|
||||
|
||||
/// Normalized reasons used when classifying filesystem sandbox denials.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FileSystemSandboxViolationReason {
|
||||
OperationNotPermitted,
|
||||
PermissionDenied,
|
||||
ReadOnlyFileSystem,
|
||||
PolicyDenied,
|
||||
FailedToWriteFile,
|
||||
SignalSyscall,
|
||||
}
|
||||
|
||||
impl FileSystemSandboxViolationReason {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::OperationNotPermitted => "operation_not_permitted",
|
||||
Self::PermissionDenied => "permission_denied",
|
||||
Self::ReadOnlyFileSystem => "read_only_file_system",
|
||||
Self::PolicyDenied => "policy_denied",
|
||||
Self::FailedToWriteFile => "failed_to_write_file",
|
||||
Self::SignalSyscall => "sigsys",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A network sandbox denial reported by the managed network proxy.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct NetworkSandboxViolation {
|
||||
pub backend: SandboxViolationBackend,
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
pub client: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub mode: Option<NetworkMode>,
|
||||
pub protocol: String,
|
||||
pub decision: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub port: Option<u16>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
impl NetworkSandboxViolation {
|
||||
pub fn from_blocked_request(blocked: &BlockedRequest) -> Self {
|
||||
Self {
|
||||
backend: SandboxViolationBackend::ManagedNetworkProxy,
|
||||
host: blocked.host.clone(),
|
||||
reason: blocked.reason.clone(),
|
||||
client: blocked.client.clone(),
|
||||
method: blocked.method.clone(),
|
||||
mode: blocked.mode,
|
||||
protocol: blocked.protocol.clone(),
|
||||
decision: blocked.decision.clone(),
|
||||
source: blocked.source.clone(),
|
||||
port: blocked.port,
|
||||
timestamp: blocked.timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classify a sandboxed process result as a filesystem sandbox violation.
|
||||
fn classify_filesystem_sandbox_violation(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
) -> Option<FileSystemSandboxViolation> {
|
||||
if exec_output.exit_code == 0 {
|
||||
return None;
|
||||
}
|
||||
let backend = match sandbox_type {
|
||||
SandboxType::None => return None,
|
||||
SandboxType::MacosSeatbelt => SandboxViolationBackend::Seatbelt,
|
||||
SandboxType::LinuxSeccomp => SandboxViolationBackend::LinuxSandbox,
|
||||
SandboxType::WindowsRestrictedToken => SandboxViolationBackend::WindowsSandbox,
|
||||
};
|
||||
|
||||
if let Some((reason, output)) = filesystem_reason_from_output(exec_output) {
|
||||
return Some(FileSystemSandboxViolation {
|
||||
backend,
|
||||
reason,
|
||||
path: extract_denied_path_from_text(output),
|
||||
output_snippet: output_snippet(output),
|
||||
});
|
||||
}
|
||||
|
||||
if QUICK_REJECT_EXIT_CODES.contains(&exec_output.exit_code) {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
if sandbox_type == SandboxType::LinuxSeccomp
|
||||
&& exec_output.exit_code == EXIT_CODE_SIGNAL_BASE + libc::SIGSYS
|
||||
{
|
||||
return Some(FileSystemSandboxViolation {
|
||||
backend,
|
||||
reason: FileSystemSandboxViolationReason::SignalSyscall,
|
||||
path: None,
|
||||
output_snippet: [
|
||||
&exec_output.stderr.text,
|
||||
&exec_output.stdout.text,
|
||||
&exec_output.aggregated_output.text,
|
||||
]
|
||||
.into_iter()
|
||||
.find(|section| !section.trim().is_empty())
|
||||
.map_or_else(String::new, |section| output_snippet(section)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Record a filesystem sandbox violation, returning the classified event when one was found.
|
||||
pub fn record_filesystem_sandbox_violation(
|
||||
sandbox_type: SandboxType,
|
||||
exec_output: &ExecToolCallOutput,
|
||||
) -> Option<FileSystemSandboxViolation> {
|
||||
let violation = classify_filesystem_sandbox_violation(sandbox_type, exec_output)?;
|
||||
record_sandbox_violation(&SandboxViolationEvent::FileSystem(violation.clone()));
|
||||
Some(violation)
|
||||
}
|
||||
|
||||
/// Record a network sandbox violation from a managed-proxy blocked request.
|
||||
pub fn record_network_sandbox_violation(blocked: &BlockedRequest) -> NetworkSandboxViolation {
|
||||
let violation = NetworkSandboxViolation::from_blocked_request(blocked);
|
||||
record_sandbox_violation(&SandboxViolationEvent::Network(violation.clone()));
|
||||
violation
|
||||
}
|
||||
|
||||
/// Emit a sandbox violation to the tracing stack.
|
||||
pub fn record_sandbox_violation(event: &SandboxViolationEvent) {
|
||||
match event {
|
||||
SandboxViolationEvent::FileSystem(violation) => {
|
||||
let path = violation.path.as_deref().unwrap_or("unknown");
|
||||
warn!(
|
||||
"recorded sandbox violation: resource=filesystem backend={} reason={} path={}",
|
||||
violation.backend.as_str(),
|
||||
violation.reason.as_str(),
|
||||
path
|
||||
);
|
||||
}
|
||||
SandboxViolationEvent::Network(violation) => {
|
||||
warn!(
|
||||
"recorded sandbox violation: resource=network backend={} protocol={} host={} port={:?} reason={} method={:?} mode={:?} client={:?} decision={:?} source={:?}",
|
||||
violation.backend.as_str(),
|
||||
violation.protocol,
|
||||
violation.host,
|
||||
violation.port,
|
||||
violation.reason,
|
||||
violation.method,
|
||||
violation.mode,
|
||||
violation.client,
|
||||
violation.decision,
|
||||
violation.source
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn filesystem_reason_from_output(
|
||||
exec_output: &ExecToolCallOutput,
|
||||
) -> Option<(FileSystemSandboxViolationReason, &str)> {
|
||||
[
|
||||
&exec_output.stderr.text,
|
||||
&exec_output.stdout.text,
|
||||
&exec_output.aggregated_output.text,
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|section| {
|
||||
let section = section.as_str();
|
||||
let lower = section.to_lowercase();
|
||||
SANDBOX_DENIED_KEYWORDS
|
||||
.iter()
|
||||
.find_map(|(reason, needle)| lower.contains(needle).then_some(*reason))
|
||||
.map(|reason| (reason, section))
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_denied_path_from_text(text: &str) -> Option<String> {
|
||||
const PATH_MARKERS: [&str; 3] = [
|
||||
": operation not permitted",
|
||||
": permission denied",
|
||||
": read-only file system",
|
||||
];
|
||||
|
||||
for line in text.lines() {
|
||||
for marker in PATH_MARKERS {
|
||||
let Some(marker_start) = line.match_indices(':').find_map(|(marker_start, _)| {
|
||||
line.get(marker_start..)
|
||||
.and_then(|suffix| suffix.get(..marker.len()))
|
||||
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(marker))
|
||||
.then_some(marker_start)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let candidate_prefix = &line[..marker_start];
|
||||
let candidate = candidate_prefix
|
||||
.rsplit_once(": ")
|
||||
.map_or(candidate_prefix, |(_, path)| path)
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'');
|
||||
if candidate.starts_with('/')
|
||||
|| candidate.starts_with("./")
|
||||
|| candidate.starts_with("../")
|
||||
{
|
||||
return Some(candidate.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn output_snippet(output: &str) -> String {
|
||||
output
|
||||
.trim()
|
||||
.chars()
|
||||
.take(OUTPUT_SNIPPET_MAX_CHARS)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "violation_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,237 @@
|
||||
use super::*;
|
||||
use codex_network_proxy::BlockedRequest;
|
||||
use codex_network_proxy::BlockedRequestArgs;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
use codex_protocol::exec_output::StreamOutput;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
fn make_exec_output(
|
||||
exit_code: i32,
|
||||
stdout: &str,
|
||||
stderr: &str,
|
||||
aggregated: &str,
|
||||
) -> ExecToolCallOutput {
|
||||
ExecToolCallOutput {
|
||||
exit_code,
|
||||
stdout: StreamOutput::new(stdout.to_string()),
|
||||
stderr: StreamOutput::new(stderr.to_string()),
|
||||
aggregated_output: StreamOutput::new(aggregated.to_string()),
|
||||
duration: Duration::from_millis(1),
|
||||
timed_out: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_legacy_denial_keywords() {
|
||||
for keyword in [
|
||||
"operation not permitted",
|
||||
"permission denied",
|
||||
"read-only file system",
|
||||
"seccomp",
|
||||
"sandbox",
|
||||
"landlock",
|
||||
"failed to write file",
|
||||
] {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", keyword, "");
|
||||
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output).is_some(),
|
||||
"{keyword}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalizes_backend_keywords_as_policy_denied() {
|
||||
for keyword in ["seccomp", "sandbox", "landlock"] {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", keyword, "");
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::LinuxSandbox,
|
||||
reason: FileSystemSandboxViolationReason::PolicyDenied,
|
||||
path: None,
|
||||
output_snippet: keyword.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_legacy_denial_ordering() {
|
||||
let quick_reject_without_keyword =
|
||||
make_exec_output(/*exit_code*/ 127, "", "command not found", "");
|
||||
let quick_reject_with_keyword =
|
||||
make_exec_output(/*exit_code*/ 127, "", "Permission denied", "");
|
||||
let zero_exit_with_keyword =
|
||||
make_exec_output(/*exit_code*/ 0, "", "Operation not permitted", "");
|
||||
let non_sandbox_with_keyword =
|
||||
make_exec_output(/*exit_code*/ 1, "", "Operation not permitted", "");
|
||||
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(
|
||||
SandboxType::LinuxSeccomp,
|
||||
&quick_reject_without_keyword
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(
|
||||
SandboxType::LinuxSeccomp,
|
||||
&quick_reject_with_keyword
|
||||
)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &zero_exit_with_keyword)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::None, &non_sandbox_with_keyword)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_filesystem_violation_with_path() {
|
||||
let output = make_exec_output(
|
||||
/*exit_code*/ 1,
|
||||
"",
|
||||
"bash: /private/tmp/denied: Operation not permitted",
|
||||
"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::OperationNotPermitted,
|
||||
path: Some("/private/tmp/denied".to_string()),
|
||||
output_snippet: "bash: /private/tmp/denied: Operation not permitted".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_filesystem_violation_with_unicode_before_marker() {
|
||||
let output = make_exec_output(
|
||||
/*exit_code*/ 1,
|
||||
"",
|
||||
"bash: /private/tmp/\u{130}-denied: Operation not permitted",
|
||||
"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::OperationNotPermitted,
|
||||
path: Some("/private/tmp/\u{130}-denied".to_string()),
|
||||
output_snippet: "bash: /private/tmp/\u{130}-denied: Operation not permitted"
|
||||
.to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_filesystem_violation_from_aggregated_output() {
|
||||
let output = make_exec_output(
|
||||
/*exit_code*/ 101,
|
||||
"",
|
||||
"",
|
||||
"cargo failed: Read-only file system when writing target",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::ReadOnlyFileSystem,
|
||||
path: None,
|
||||
output_snippet: "cargo failed: Read-only file system when writing target".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_output_snippet_on_the_stream_that_matched() {
|
||||
let output = make_exec_output(
|
||||
/*exit_code*/ 1,
|
||||
"bash: /private/tmp/denied: Permission denied",
|
||||
"unrelated warning",
|
||||
"unrelated warning\nbash: /private/tmp/denied: Permission denied",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::MacosSeatbelt, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::Seatbelt,
|
||||
reason: FileSystemSandboxViolationReason::PermissionDenied,
|
||||
path: Some("/private/tmp/denied".to_string()),
|
||||
output_snippet: "bash: /private/tmp/denied: Permission denied".to_string(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn classifies_linux_sigsys_exit() {
|
||||
let output = make_exec_output(
|
||||
/*exit_code*/ EXIT_CODE_SIGNAL_BASE + libc::SIGSYS,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
classify_filesystem_sandbox_violation(SandboxType::LinuxSeccomp, &output),
|
||||
Some(FileSystemSandboxViolation {
|
||||
backend: SandboxViolationBackend::LinuxSandbox,
|
||||
reason: FileSystemSandboxViolationReason::SignalSyscall,
|
||||
path: None,
|
||||
output_snippet: String::new(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_non_sandbox_mode() {
|
||||
let output = make_exec_output(/*exit_code*/ 1, "", "Operation not permitted", "");
|
||||
|
||||
assert!(classify_filesystem_sandbox_violation(SandboxType::None, &output).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_blocked_request_to_network_violation() {
|
||||
let blocked = BlockedRequest::new(BlockedRequestArgs {
|
||||
host: "example.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
client: Some("curl".to_string()),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
protocol: "https".to_string(),
|
||||
decision: Some("block".to_string()),
|
||||
source: Some("policy".to_string()),
|
||||
port: Some(443),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
NetworkSandboxViolation::from_blocked_request(&blocked),
|
||||
NetworkSandboxViolation {
|
||||
backend: SandboxViolationBackend::ManagedNetworkProxy,
|
||||
host: "example.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
client: Some("curl".to_string()),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
protocol: "https".to_string(),
|
||||
decision: Some("block".to_string()),
|
||||
source: Some("policy".to_string()),
|
||||
port: Some(443),
|
||||
timestamp: blocked.timestamp,
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user