use crate::agent::Agent;
use crate::agent::FileLoader;
use crate::error::JacsError;
use std::fs;
use tracing::{error, info};
use std::fs::Permissions;
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use walkdir::WalkDir;
const _JACS_ENABLE_FILESYSTEM_QUARANTINE: &str = "JACS_ENABLE_FILESYSTEM_QUARANTINE";
pub trait SecurityTraits {
fn use_security(&self) -> bool;
fn use_fs_security(&self) -> bool;
fn check_data_directory(&self) -> Result<(), JacsError>;
fn is_executable(&self, path: &std::path::Path) -> bool;
fn quarantine_file(&self, file_path: &Path) -> Result<(), JacsError>;
fn mark_file_not_executable(&self, path: &std::path::Path) -> Result<(), JacsError>;
}
impl SecurityTraits for Agent {
fn check_data_directory(&self) -> Result<(), JacsError> {
if !self.use_security() {
info!(
"Filesystem quarantine is disabled. Set JACS_ENABLE_FILESYSTEM_QUARANTINE=true to enable. \
Note: this does NOT affect cryptographic signing or verification."
);
return Ok(());
}
if !self.use_fs_security() {
info!("filesystem security is off because the config is not using filestyem ");
return Ok(());
}
let data_dir = self
.config
.as_ref()
.unwrap()
.jacs_data_directory()
.as_deref()
.unwrap_or_default();
let dir = Path::new(&data_dir);
for entry in WalkDir::new(dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
{
if self.is_executable(entry.path()) {
let _ = self.quarantine_file(entry.path());
}
}
Ok(())
}
fn use_security(&self) -> bool {
matches!(self.config.as_ref().unwrap().jacs_use_security(), Some(value) if matches!(value.to_lowercase().as_str(), "true" | "1"))
}
fn use_fs_security(&self) -> bool {
self.use_filesystem()
}
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
fn mark_file_not_executable(&self, path: &std::path::Path) -> Result<(), JacsError> {
std::fs::set_permissions(Path::new(path), Permissions::from_mode(0o600))?;
Ok(())
}
#[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))]
fn mark_file_not_executable(&self, path: &std::path::Path) -> Result<(), JacsError> {
tracing::warn!(
"Windows: Cannot modify execute permissions for {:?}. File has been quarantined.",
path
);
Ok(())
}
#[cfg(target_arch = "wasm32")]
fn mark_file_not_executable(&self, _path: &std::path::Path) -> Result<(), JacsError> {
Ok(())
}
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
fn is_executable(&self, path: &std::path::Path) -> bool {
if !self.use_fs_security() {
info!(
"is_executable not possible because security is off: {}",
path.to_string_lossy()
);
return false;
}
let metadata = match path.metadata() {
Ok(metadata) => metadata,
Err(_) => return false,
};
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(all(not(target_arch = "wasm32"), target_os = "windows"))]
fn is_executable(&self, path: &std::path::Path) -> bool {
use std::io::Read;
if !self.use_fs_security() {
info!("is_executable check on Windows: {}", path.to_string_lossy());
return false;
}
if let Some(ext) = path.extension() {
let ext_lower = ext.to_str().unwrap_or("").to_lowercase();
if matches!(
ext_lower.as_str(),
"exe" | "bat" | "cmd" | "ps1" | "com" | "scr"
) {
return true;
}
}
if let Ok(mut file) = std::fs::File::open(path) {
let mut buffer = [0u8; 2];
if file.read_exact(&mut buffer).is_ok() && buffer == [0x4D, 0x5A] {
return true;
}
}
false
}
#[cfg(target_arch = "wasm32")]
fn is_executable(&self, _path: &std::path::Path) -> bool {
false
}
fn quarantine_file(&self, file_path: &Path) -> Result<(), JacsError> {
if !self.use_fs_security() {
info!(
"quarantine not possible because filesystem is not used: {}",
file_path.to_string_lossy()
);
return Ok(());
}
let data_dir = self
.config
.as_ref()
.unwrap()
.jacs_data_directory()
.as_deref()
.unwrap_or_default();
let quarantine_dir = Path::new(&data_dir).join("quarantine");
if !quarantine_dir.exists() {
fs::create_dir_all(&quarantine_dir).map_err(|e| {
format!(
"Failed to create quarantine directory '{}': {}. \
Check that the parent directory exists and has write permissions.",
quarantine_dir.display(),
e
)
})?;
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "windows")))]
{
let permissions = Permissions::from_mode(0o755);
fs::set_permissions(&quarantine_dir, permissions).map_err(|e| {
format!(
"Failed to set permissions on quarantine directory '{}': {}",
quarantine_dir.display(),
e
)
})?;
}
}
let file_name = match file_path.file_name() {
Some(name) => name,
None => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Invalid file path",
)
.into());
}
};
let dest_path = quarantine_dir.join(file_name);
error!(
"security: moving {:?} to {:?} as it may be executable.",
file_name, dest_path
);
fs::rename(file_path, &dest_path)?;
self.mark_file_not_executable(&dest_path)?;
Ok(())
}
}