use std::path::Path;
use crate::checks::CoreLayer;
use crate::operation::{MetadataParams, Operation, XattrNamespace};
use crate::state::path::StatResult;
use crate::state::{Probe, SystemState};
use super::scoring;
use super::{Fix, FixAction};
pub fn mount_fixes(state: &SystemState) -> Vec<Fix> {
let Some(target_idx) = state.operation.target_component(state.walk.len()) else {
return Vec::new();
};
let Some(mount_idx) = state.walk[target_idx].mount else {
return Vec::new();
};
let Some(entry) = state.mounts.0.get(mount_idx) else {
return Vec::new();
};
let mut fixes = Vec::new();
if entry.options.read_only && is_write_op(state.operation) {
fixes.push(make_remount_fix(&entry.mountpoint, "rw", "read-write"));
}
if entry.options.noexec && state.operation == Operation::Execute {
fixes.push(make_remount_fix(&entry.mountpoint, "exec", "with exec"));
}
fixes
}
fn make_remount_fix(mountpoint: &std::path::Path, options: &str, desc_suffix: &str) -> Fix {
Fix {
layer: CoreLayer::Mount,
action: FixAction::Remount {
mountpoint: mountpoint.to_path_buf(),
options: options.into(),
},
impact: scoring::REMOUNT,
description: format!("remount {} {desc_suffix}", mountpoint.display()),
}
}
pub fn fsflags_fixes(state: &SystemState) -> Vec<Fix> {
let Some(target_idx) = state.operation.target_component(state.walk.len()) else {
return Vec::new();
};
let comp = &state.walk[target_idx];
let Probe::Known(flags) = &comp.flags else {
return Vec::new();
};
let path = &comp.path;
let mut fixes = Vec::new();
if flags.immutable {
fixes.push(make_chattr_fix(path, "-i", "immutable"));
}
if flags.append_only && !flags.immutable {
fixes.push(make_chattr_fix(path, "-a", "append-only"));
}
fixes
}
fn make_chattr_fix(path: &std::path::Path, flags: &str, name: &str) -> Fix {
Fix {
layer: CoreLayer::FsFlags,
action: FixAction::Chattr {
path: path.to_path_buf(),
flags: flags.into(),
},
impact: scoring::REMOVE_FS_FLAG,
description: format!("remove {name} flag from {}", path.display()),
}
}
pub fn traversal_fixes(state: &SystemState, component_idx: usize) -> Vec<Fix> {
let Some(comp) = state.walk.get(component_idx) else {
return Vec::new();
};
let path = &comp.path;
let uid = state.subject.uid;
vec![
Fix {
layer: CoreLayer::Traversal,
action: FixAction::SetAcl {
path: path.clone(),
entry: format!("u:{uid}:--x"),
},
impact: scoring::ACL_USER_GRANT,
description: format!("grant execute to uid {uid} on {} via ACL", path.display()),
},
Fix {
layer: CoreLayer::Traversal,
action: FixAction::Chmod {
path: path.clone(),
mode_change: "o+x".into(),
},
impact: scoring::CHMOD_OTHER,
description: format!("grant other-execute on {}", path.display()),
},
]
}
pub fn dac_fixes(state: &SystemState) -> Vec<Fix> {
let Some(target_idx) = state.operation.target_component(state.walk.len()) else {
return Vec::new();
};
let comp = &state.walk[target_idx];
let Probe::Known(stat) = &comp.stat else {
return Vec::new();
};
let perm_char = op_to_perm_char(state.operation);
let uid = state.subject.uid;
let path = &comp.path;
let mut fixes = Vec::new();
push_acl_fix(&mut fixes, path, uid, perm_char);
push_group_fix(&mut fixes, stat, state, path, perm_char);
push_other_fix(&mut fixes, path, perm_char);
fixes
}
pub fn acl_fixes(state: &SystemState) -> Vec<Fix> {
let Some(target_idx) = state.operation.target_component(state.walk.len()) else {
return Vec::new();
};
let comp = &state.walk[target_idx];
let perm_char = op_to_perm_char(state.operation);
let uid = state.subject.uid;
vec![Fix {
layer: CoreLayer::Acl,
action: FixAction::SetAcl {
path: comp.path.clone(),
entry: format!("u:{uid}:{perm_char}"),
},
impact: scoring::ACL_USER_GRANT,
description: format!(
"grant {perm_char} to uid {uid} on {} via ACL",
comp.path.display()
),
}]
}
fn push_acl_fix(fixes: &mut Vec<Fix>, path: &std::path::Path, uid: u32, perm: char) {
fixes.push(Fix {
layer: CoreLayer::Dac,
action: FixAction::SetAcl {
path: path.to_path_buf(),
entry: format!("u:{uid}:{perm}"),
},
impact: scoring::ACL_USER_GRANT,
description: format!("grant {perm} to uid {uid} on {} via ACL", path.display()),
});
}
fn push_group_fix(
fixes: &mut Vec<Fix>,
stat: &StatResult,
state: &SystemState,
path: &std::path::Path,
perm: char,
) {
if state.subject.in_group(stat.gid) {
fixes.push(Fix {
layer: CoreLayer::Dac,
action: FixAction::Chmod {
path: path.to_path_buf(),
mode_change: format!("g+{perm}"),
},
impact: scoring::CHMOD_GROUP,
description: format!("grant group-{perm} on {}", path.display()),
});
}
}
fn push_other_fix(fixes: &mut Vec<Fix>, path: &std::path::Path, perm: char) {
fixes.push(Fix {
layer: CoreLayer::Dac,
action: FixAction::Chmod {
path: path.to_path_buf(),
mode_change: format!("o+{perm}"),
},
impact: scoring::CHMOD_OTHER,
description: format!("grant other-{perm} on {}", path.display()),
});
}
pub fn metadata_fixes(state: &SystemState, _params: &MetadataParams) -> Vec<Fix> {
let Some(target_idx) = state.operation.target_component(state.walk.len()) else {
return Vec::new();
};
let target_path = state.walk[target_idx].path.clone();
match state.operation {
Operation::Chmod => chmod_metadata_fixes(state, &target_path),
Operation::ChownUid | Operation::ChownGid => chown_metadata_fixes(),
Operation::SetXattr { namespace } => {
setxattr_metadata_fixes(state, &target_path, namespace)
}
Operation::Read
| Operation::Write
| Operation::Execute
| Operation::Delete
| Operation::Create
| Operation::Stat => Vec::new(),
}
}
fn chmod_metadata_fixes(state: &SystemState, path: &Path) -> Vec<Fix> {
vec![
make_chown_fix(path, state.subject.uid, 4),
make_grant_cap_fix("cap_fowner", 5, "grant CAP_FOWNER to the process"),
]
}
fn chown_metadata_fixes() -> Vec<Fix> {
vec![make_grant_cap_fix(
"cap_chown",
5,
"grant CAP_CHOWN to the process",
)]
}
fn setxattr_metadata_fixes(
state: &SystemState,
path: &Path,
namespace: XattrNamespace,
) -> Vec<Fix> {
match namespace {
XattrNamespace::User => vec![make_chown_fix(path, state.subject.uid, 4)],
XattrNamespace::Trusted | XattrNamespace::Security => {
vec![make_cap_sys_admin_fix()]
}
XattrNamespace::SystemPosixAcl => vec![
make_chown_fix(path, state.subject.uid, 4),
make_grant_cap_fix("cap_fowner", 5, "grant CAP_FOWNER to the process"),
],
}
}
fn make_cap_sys_admin_fix() -> Fix {
make_grant_cap_fix(
"cap_sys_admin",
6,
"grant CAP_SYS_ADMIN to the process (broad system administration capability)",
)
}
fn make_chown_fix(path: &Path, uid: u32, impact: u8) -> Fix {
Fix {
layer: CoreLayer::Metadata,
action: FixAction::Chown {
path: path.to_path_buf(),
owner: Some(uid),
group: None,
},
impact,
description: format!("transfer file ownership to subject (uid={uid})"),
}
}
fn make_grant_cap_fix(capability: &str, impact: u8, description: &str) -> Fix {
Fix {
layer: CoreLayer::Metadata,
action: FixAction::GrantCap {
path: None,
capability: capability.into(),
},
impact,
description: description.into(),
}
}
fn is_write_op(op: Operation) -> bool {
matches!(op, Operation::Write | Operation::Delete | Operation::Create)
}
fn op_to_perm_char(op: Operation) -> char {
match op {
Operation::Read | Operation::Stat => 'r',
Operation::Write | Operation::Delete | Operation::Create => 'w',
Operation::Execute => 'x',
Operation::Chmod
| Operation::ChownUid
| Operation::ChownGid
| Operation::SetXattr { .. } => {
unreachable!("is_metadata() guard must precede op_to_perm_char()")
}
}
}