Skip to content

Commit

Permalink
feat: filesystem exception whitelist merging
Browse files Browse the repository at this point in the history
  • Loading branch information
desbma committed Jan 18, 2025
1 parent 65e8c74 commit 2263ab4
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 44 deletions.
15 changes: 12 additions & 3 deletions src/cl.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Command line interface
use std::path::PathBuf;
use std::{num::NonZeroUsize, path::PathBuf};

use clap::Parser;

Expand Down Expand Up @@ -35,6 +35,10 @@ pub(crate) struct HardeningOptions {
/// Enable whitelist-based filesystem hardening
#[arg(short = 'w', long, default_value_t)]
pub filesystem_whitelisting: bool,
/// When using whitelist-based filesystem hardening, if path whitelist is longer than this value,
/// try to merge paths with the same parent
#[arg(long, default_value = "5")]
pub merge_paths_threshold: NonZeroUsize,
}

impl HardeningOptions {
Expand All @@ -45,6 +49,8 @@ impl HardeningOptions {
mode: HardeningMode::Safe,
network_firewalling: false,
filesystem_whitelisting: false,
#[expect(clippy::unwrap_used)]
merge_paths_threshold: NonZeroUsize::new(1).unwrap(),
}
}

Expand All @@ -54,19 +60,22 @@ impl HardeningOptions {
mode: HardeningMode::Aggressive,
network_firewalling: true,
filesystem_whitelisting: true,
#[expect(clippy::unwrap_used)]
merge_paths_threshold: NonZeroUsize::new(usize::MAX).unwrap(),
}
}

pub(crate) fn to_cmdline(&self) -> String {
format!(
"-m {}{}{}",
"-m {}{}{} --merge-paths-threshold {}",
self.mode,
if self.network_firewalling { " -n" } else { "" },
if self.filesystem_whitelisting {
" -w"
} else {
""
}
},
self.merge_paths_threshold
)
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ fn main() -> anyhow::Result<()> {
bincode::serialize_into(file, &actions)?;
} else {
// Resolve
let resolved_opts = systemd::resolve(&sd_opts, &actions);
let resolved_opts = systemd::resolve(&sd_opts, &actions, &hardening_opts);

// Report
systemd::report_options(resolved_opts);
Expand All @@ -136,7 +136,7 @@ fn main() -> anyhow::Result<()> {
log::debug!("{actions:?}");

// Resolve
let resolved_opts = systemd::resolve(&sd_opts, &actions);
let resolved_opts = systemd::resolve(&sd_opts, &actions, &hardening_opts);

// Report
systemd::report_options(resolved_opts);
Expand Down
126 changes: 116 additions & 10 deletions src/systemd/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::{
borrow::ToOwned,
collections::{HashMap, HashSet},
fmt, iter,
num::NonZeroUsize,
os::unix::ffi::OsStrExt as _,
path::{Path, PathBuf},
str::FromStr,
Expand All @@ -27,9 +28,10 @@ use crate::{
#[derive(Debug)]
pub(crate) struct OptionUpdater {
/// Generate a new option effect compatible with the previously incompatible action
pub effect: fn(&OptionValueEffect, &ProgramAction) -> Option<OptionValueEffect>,
pub effect:
fn(&OptionValueEffect, &ProgramAction, &HardeningOptions) -> Option<OptionValueEffect>,
/// Generate new options from the new effect
pub options: fn(&OptionValueEffect) -> Vec<OptionWithValue>,
pub options: fn(&OptionValueEffect, &HardeningOptions) -> Vec<OptionWithValue>,
}

/// Systemd option with its possibles values, and their effect
Expand Down Expand Up @@ -812,6 +814,49 @@ static SYSCALL_CLASSES: LazyLock<HashMap<&'static str, HashSet<&'static str>>> =
])
});

fn merge_similar_paths(paths: &[PathBuf], threshold: NonZeroUsize) -> Vec<PathBuf> {
if paths.len() <= threshold.get() {
paths.to_vec()
} else {
let mut children: HashMap<PathBuf, HashSet<PathBuf>> = HashMap::new();
for path in paths {
let ancestors: Vec<_> = path.ancestors().map(Path::to_path_buf).collect();
let mut parent: Option<PathBuf> = None;
for dir in ancestors.into_iter().rev() {
if let Some(parent) = parent.as_ref() {
children
.entry(parent.to_owned())
.or_default()
.insert(dir.clone());
}
parent = Some(dir);
}
}
let initial_candidates = vec![PathBuf::from("/")];
let mut candidates = initial_candidates.clone();
loop {
let mut advancing = false;
let mut new_candidates = Vec::with_capacity(candidates.len());
for candidate in &candidates {
if let Some(candidate_children) = children.get(candidate) {
new_candidates.extend(candidate_children.iter().cloned());
advancing |= !candidate_children.is_empty();
}
}
if !advancing || new_candidates.len() > threshold.get() {
break;
}
candidates = new_candidates;
}
if candidates == initial_candidates {
paths.to_vec()
} else {
candidates.sort_unstable();
candidates
}
}
}

#[expect(clippy::too_many_lines)]
pub(crate) fn build_options(
systemd_version: &SystemdVersion,
Expand Down Expand Up @@ -1121,7 +1166,7 @@ pub(crate) fn build_options(
})),
}],
updater: Some(OptionUpdater {
effect: |effect, action| match effect {
effect: |effect, action, _hopts| match effect {
OptionValueEffect::DenyWrite(PathDescription::Base { base, exceptions }) => {
let new_exception = match action {
ProgramAction::Write(action_path) => Some(action_path.to_owned()),
Expand All @@ -1145,7 +1190,7 @@ pub(crate) fn build_options(
}
_ => None,
},
options: |effect| match effect {
options: |effect, hopts| match effect {
OptionValueEffect::DenyWrite(PathDescription::Base { base, exceptions }) => {
vec![
OptionWithValue {
Expand All @@ -1156,10 +1201,13 @@ pub(crate) fn build_options(
OptionWithValue {
name: "ReadWritePaths".to_owned(),
value: OptionValue::List {
values: exceptions
.iter()
.filter_map(|p| p.to_str().map(ToOwned::to_owned))
.collect(),
values: merge_similar_paths(
exceptions,
hopts.merge_paths_threshold,
)
.iter()
.filter_map(|p| p.to_str().map(ToOwned::to_owned))
.collect(),
value_if_empty: None,
prefix: "",
repeat_option: false,
Expand Down Expand Up @@ -1330,7 +1378,7 @@ pub(crate) fn build_options(
),
}],
updater: hardening_opts.network_firewalling.then_some(OptionUpdater {
effect: |e, a| {
effect: |e, a, _| {
let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(effect_na)) = e
else {
unreachable!();
Expand All @@ -1353,7 +1401,7 @@ pub(crate) fn build_options(
}),
))
},
options: |e| {
options: |e, _| {
let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(denied_na)) = e
else {
unreachable!();
Expand Down Expand Up @@ -1640,3 +1688,61 @@ pub(crate) fn build_options(
log::debug!("{options:#?}");
options
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_merge_similar_paths() {
assert_eq!(
merge_similar_paths(
&[
PathBuf::from("/a/ab/ab1"),
PathBuf::from("/a/ab/ab2"),
PathBuf::from("/a/ab/ab3"),
PathBuf::from("/a/ab/ab4/abc")
],
NonZeroUsize::new(2).unwrap()
),
vec![PathBuf::from("/a/ab")]
);
assert_eq!(
merge_similar_paths(
&[
PathBuf::from("/a1/ab/ab1"),
PathBuf::from("/a2/ab/ab2"),
PathBuf::from("/a3/ab/ab3")
],
NonZeroUsize::new(2).unwrap()
),
vec![
PathBuf::from("/a1/ab/ab1"),
PathBuf::from("/a2/ab/ab2"),
PathBuf::from("/a3/ab/ab3")
]
);
assert_eq!(
merge_similar_paths(
&[
PathBuf::from("/a/aa/ab1"),
PathBuf::from("/a/ab/ab2"),
PathBuf::from("/a/ac/ab3")
],
NonZeroUsize::new(2).unwrap()
),
vec![PathBuf::from("/a")]
);
assert_eq!(
merge_similar_paths(
&[
PathBuf::from("/a/aa/ab1"),
PathBuf::from("/a/aa/ab2"),
PathBuf::from("/a/ab/ab3")
],
NonZeroUsize::new(2).unwrap()
),
vec![PathBuf::from("/a/aa"), PathBuf::from("/a/ab")]
);
}
}
Loading

0 comments on commit 2263ab4

Please sign in to comment.