use crate::engine::error::{Error, Mechanism, errno_name};
use serde::Serialize;
#[cfg(target_os = "linux")]
use std::collections::BTreeMap;
use std::error::Error as StdError;
#[cfg(target_os = "linux")]
use std::path::PathBuf;
const INTERNAL_ERROR: &str = "INTERNAL_ERROR";
#[cfg(target_os = "linux")]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum TrapOperation {
Read,
Write,
}
#[cfg(target_os = "linux")]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, strum_macros::IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub(crate) enum NetworkOperation {
Connect,
Bind,
}
#[cfg(target_os = "linux")]
impl NetworkOperation {
fn syscall(self) -> &'static str {
self.into()
}
}
#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
pub(crate) struct ProcessContext {
pub(crate) pid: u32,
pub(crate) exe: Option<String>,
pub(crate) cwd: Option<String>,
}
#[cfg(target_os = "linux")]
#[derive(Debug, Serialize)]
pub(crate) struct FilesystemTrap {
pub(crate) code: &'static str,
pub(crate) state: &'static str,
pub(crate) query_id: String,
pub(crate) operation: TrapOperation,
pub(crate) path: String,
pub(crate) requested_path: String,
pub(crate) syscall: &'static str,
pub(crate) errno: &'static str,
pub(crate) flags: Vec<&'static str>,
pub(crate) reason: &'static str,
pub(crate) suggested_grant: BTreeMap<&'static str, String>,
pub(crate) process: ProcessContext,
pub(crate) mechanism: Mechanism,
}
#[cfg(target_os = "linux")]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub(crate) struct FilesystemDenial {
pub(crate) operation: TrapOperation,
pub(crate) path: PathBuf,
pub(crate) requested_path: PathBuf,
pub(crate) syscall: &'static str,
pub(crate) flags: Vec<&'static str>,
pub(crate) reason: &'static str,
pub(crate) process: ProcessContext,
}
#[cfg(target_os = "linux")]
#[derive(Debug, Serialize)]
pub(crate) struct NetworkTrap {
pub(crate) code: &'static str,
pub(crate) state: &'static str,
pub(crate) query_id: String,
pub(crate) operation: &'static str,
pub(crate) target: String,
pub(crate) syscall: &'static str,
pub(crate) errno: &'static str,
pub(crate) mechanism: Mechanism,
pub(crate) process: ProcessContext,
}
#[derive(Debug, Serialize)]
pub(crate) struct LaunchTrap {
pub(crate) code: &'static str,
pub(crate) program: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) errno: Option<&'static str>,
pub(crate) message: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct UsageTrap {
pub(crate) code: &'static str,
pub(crate) message: String,
}
#[derive(Debug, Serialize)]
pub(crate) struct InternalTrap {
pub(crate) code: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) mechanism: Option<Mechanism>,
pub(crate) message: String,
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub(crate) enum Trap {
#[cfg(target_os = "linux")]
Filesystem(Box<FilesystemTrap>),
#[cfg(target_os = "linux")]
Network(Box<NetworkTrap>),
Launch(Box<LaunchTrap>),
Usage(Box<UsageTrap>),
Internal(Box<InternalTrap>),
}
impl Trap {
#[cfg(target_os = "linux")]
pub(crate) fn filesystem(denial: FilesystemDenial, query_id: Option<u64>) -> Self {
let FilesystemDenial {
operation,
path,
requested_path,
syscall,
flags,
reason,
process,
} = denial;
let grant_key = match operation {
TrapOperation::Read => "allowRead",
TrapOperation::Write => "allowWrite",
};
let path = path.to_string_lossy().into_owned();
let requested_path = requested_path.to_string_lossy().into_owned();
let mut suggested_grant = BTreeMap::new();
suggested_grant.insert(grant_key, path.clone());
Self::Filesystem(Box::new(FilesystemTrap {
code: Error::FilesystemDenied.code(),
state: if query_id.is_some() { "query" } else { "info" },
query_id: query_id.unwrap_or(0).to_string(),
operation,
path,
requested_path,
syscall,
errno: denial_errno(),
flags,
reason,
suggested_grant,
process,
mechanism: Mechanism::Seccomp,
}))
}
#[cfg(target_os = "linux")]
pub(crate) fn network(
operation: NetworkOperation,
target: String,
process: ProcessContext,
query_id: Option<u64>,
) -> Self {
let syscall = operation.syscall();
Self::Network(Box::new(NetworkTrap {
code: Error::NetworkDenied.code(),
state: if query_id.is_some() { "query" } else { "info" },
query_id: query_id.unwrap_or(0).to_string(),
operation: syscall,
target,
syscall,
errno: denial_errno(),
mechanism: Mechanism::Seccomp,
process,
}))
}
pub(crate) fn from_error(error: &Error) -> Self {
match error {
Error::Usage { message } => Self::Usage(Box::new(UsageTrap {
code: error.code(),
message: message.clone(),
})),
Error::LaunchFailed { tool, source } => Self::Launch(Box::new(LaunchTrap {
code: error.code(),
program: tool.to_string_lossy().into_owned(),
errno: error.errno().and_then(errno_name),
message: source.to_string(),
})),
Error::SandboxSetupFailed { mechanism, .. } => Self::Internal(Box::new(InternalTrap {
code: error.code(),
mechanism: Some(*mechanism),
message: message(error),
})),
_ => Self::Internal(Box::new(InternalTrap {
code: error.code(),
mechanism: None,
message: message(error),
})),
}
}
pub(crate) fn internal(message: String) -> Self {
Self::Internal(Box::new(InternalTrap {
code: INTERNAL_ERROR,
mechanism: None,
message,
}))
}
pub(crate) fn json_line(&self) -> String {
match serde_json::to_string(self) {
Ok(line) => line,
Err(error) => {
log::error!("trap: serialize: {error}");
r#"{"kind":"internal","code":"INTERNAL_ERROR","message":"failed to serialize trap"}"#
.to_owned()
}
}
}
pub(crate) fn emit_json(line: &str) {
eprintln!("{line}");
}
pub(crate) fn emit(&self) {
Self::emit_json(&self.json_line());
}
}
#[cfg(target_os = "linux")]
fn denial_errno() -> &'static str {
errno_name(Error::DENIAL_ERRNO).unwrap_or("EACCES")
}
fn message(error: &Error) -> String {
let mut message = error.detail().unwrap_or_default();
let mut cause = StdError::source(error);
while let Some(source) = cause {
if !message.is_empty() {
message.push_str(": ");
}
message.push_str(&source.to_string());
cause = source.source();
}
if message.is_empty() {
return error.code().to_owned();
}
message
}