From fc6969181148176a3f3876ee66e251ed3d135975 Mon Sep 17 00:00:00 2001 From: desbma-s1n Date: Thu, 14 Nov 2024 15:59:40 +0100 Subject: [PATCH] feat: changeable effects --- src/summarize.rs | 235 ++++++++++++++++++++++++++++++++++++---- src/systemd/options.rs | 81 ++++++++++++++ src/systemd/resolver.rs | 231 +++++++++++++++++++++++++++++---------- tests/cl.rs | 35 ++++++ 4 files changed, 508 insertions(+), 74 deletions(-) diff --git a/src/summarize.rs b/src/summarize.rs index 4fccf9e..da4d000 100644 --- a/src/summarize.rs +++ b/src/summarize.rs @@ -3,8 +3,12 @@ use std::{ collections::{HashMap, HashSet}, ffi::OsStr, + fmt::{self, Display}, + num::NonZeroU16, + ops::{Add, RangeInclusive, Sub}, os::unix::ffi::OsStrExt, path::{Path, PathBuf}, + slice, sync::LazyLock, }; @@ -48,7 +52,7 @@ pub(crate) struct NetworkActivity { pub af: SetSpecifier, pub proto: SetSpecifier, pub kind: SetSpecifier, - pub local_port: CountableSetSpecifier, + pub local_port: CountableSetSpecifier, } /// Quantify something that is done or denied @@ -60,7 +64,7 @@ pub(crate) enum SetSpecifier { All, } -impl SetSpecifier { +impl SetSpecifier { fn contains_one(&self, needle: &T) -> bool { match self { Self::None => false, @@ -78,19 +82,42 @@ impl SetSpecifier { Self::All => !matches!(other, Self::None), } } + + pub(crate) fn elements(&self) -> &[T] { + match self { + SetSpecifier::None => &[], + SetSpecifier::One(e) => slice::from_ref(e), + SetSpecifier::Some(es) => es.as_slice(), + SetSpecifier::All => unimplemented!(), + } + } } pub(crate) trait ValueCounted { fn value_count() -> usize; + + fn min_value() -> Self; + + fn max_value() -> Self; + + fn one() -> Self; } -impl ValueCounted for u16 { - fn value_count() -> usize { - Self::MAX as usize - Self::MIN as usize + 1 - } +/// Quantify something that is done or denied +#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) enum CountableSetSpecifier { + None, + One(T), + // Elements must be ordered + Some(Vec), + // Elements must be ordered + AllExcept(Vec), + All, } -impl CountableSetSpecifier { +impl + Add> + CountableSetSpecifier +{ fn contains_one(&self, needle: &T) -> bool { match self { Self::None => false, @@ -116,16 +143,69 @@ impl CountableSetSpecifier { Self::All => !matches!(other, Self::None), } } -} -/// Quantify something that is done or denied -#[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize)] -pub(crate) enum CountableSetSpecifier { - None, - One(T), - Some(Vec), - AllExcept(Vec), - All, + /// Remove a single element from the set + /// The element to remove **must** be in the set, otherwise may panic + #[expect(clippy::unwrap_used)] + pub(crate) fn remove(&mut self, to_rm: &T) { + debug_assert!(self.contains_one(to_rm)); + match self { + Self::None => unreachable!(), + Self::One(_) => { + *self = Self::None; + } + Self::Some(es) => { + let idx = es.iter().position(|e| e == to_rm).unwrap(); + es.remove(idx); + } + Self::AllExcept(excs) => { + let idx = excs.binary_search(to_rm).unwrap_err(); + excs.insert(idx, to_rm.to_owned()); + } + Self::All => { + *self = Self::AllExcept(vec![to_rm.to_owned()]); + } + } + } + + pub(crate) fn ranges(&self) -> Vec> { + match self { + CountableSetSpecifier::None => vec![], + CountableSetSpecifier::One(e) => vec![e.to_owned()..=e.to_owned()], + CountableSetSpecifier::Some(es) => { + // Build single element ranges, we could merge adjacent elements, but + // the effort has very little upsides + es.iter().map(|e| e.to_owned()..=e.to_owned()).collect() + } + CountableSetSpecifier::AllExcept(excs) => { + let mut ranges = Vec::with_capacity(excs.len() + 1); + let mut start = None; + for exc in excs { + if *exc != T::min_value() { + let cur_start = start.unwrap_or_else(|| T::min_value()); + let cur_end = exc.to_owned() - T::one(); + let r = cur_start..=cur_end; + if !r.is_empty() { + ranges.push(r); + } + } + if *exc == T::max_value() { + start = None; + } else { + start = Some(exc.to_owned() + T::one()); + } + } + if let Some(start) = start { + let r = start..=T::max_value(); + if !r.is_empty() { + ranges.push(r); + } + } + ranges + } + CountableSetSpecifier::All => vec![T::min_value()..=T::max_value()], + } + } } /// Socket activity @@ -139,6 +219,55 @@ pub(crate) enum NetworkActivityKind { // Recv, } +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +pub(crate) struct NetworkPort(NonZeroU16); + +impl ValueCounted for NetworkPort { + fn value_count() -> usize { + // 0 is excluded + u16::MAX as usize - u16::MIN as usize + } + + fn one() -> Self { + #[expect(clippy::unwrap_used)] + Self(1_u16.try_into().unwrap()) + } + + fn min_value() -> Self { + #[expect(clippy::unwrap_used)] + Self(1_u16.try_into().unwrap()) + } + + fn max_value() -> Self { + #[expect(clippy::unwrap_used)] + Self(u16::MAX.try_into().unwrap()) + } +} + +impl Sub for NetworkPort { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + #[expect(clippy::unwrap_used)] + Self(self.0.get().sub(rhs.0.get()).try_into().unwrap()) + } +} + +impl Add for NetworkPort { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + #[expect(clippy::unwrap_used)] + Self(self.0.get().add(rhs.0.get()).try_into().unwrap()) + } +} + +impl Display for NetworkPort { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + /// Meta structure to group syscalls that have similar summary handling /// and store argument indexes enum SyscallInfo { @@ -524,8 +653,14 @@ where .. })) => { - #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - CountableSetSpecifier::One(*port_val as u16) + #[expect( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::unwrap_used + )] + CountableSetSpecifier::One(NetworkPort( + (*port_val as u16).try_into().unwrap(), + )) } _ => todo!(), }, @@ -679,7 +814,7 @@ where Ok(actions) } -#[expect(clippy::unreadable_literal)] +#[expect(clippy::unreadable_literal, clippy::shadow_unrelated)] #[cfg(test)] mod tests { use super::*; @@ -782,4 +917,66 @@ mod tests { ] ); } + + #[test] + fn test_set_ranges() { + let port = |p: u16| NetworkPort(p.try_into().unwrap()); + let set: CountableSetSpecifier = CountableSetSpecifier::None; + assert_eq!(set.ranges(), vec![]); + + for v in [1, 1234, u16::MAX] { + let set: CountableSetSpecifier = CountableSetSpecifier::One(port(v)); + assert_eq!(set.ranges(), vec![port(v)..=port(v)]); + } + + for v in [1, 1234, u16::MAX] { + let set: CountableSetSpecifier = + CountableSetSpecifier::Some(vec![port(v)]); + assert_eq!(set.ranges(), vec![port(v)..=port(v)]); + } + + let set: CountableSetSpecifier = + CountableSetSpecifier::Some(vec![port(1234), port(5678)]); + assert_eq!( + set.ranges(), + vec![port(1234)..=port(1234), port(5678)..=port(5678)] + ); + + let set: CountableSetSpecifier = + CountableSetSpecifier::AllExcept(vec![port(1)]); + assert_eq!(set.ranges(), vec![port(2)..=port(u16::MAX)]); + + let set: CountableSetSpecifier = + CountableSetSpecifier::AllExcept(vec![port(u16::MAX)]); + assert_eq!(set.ranges(), vec![port(1)..=port(u16::MAX - 1)]); + + let set: CountableSetSpecifier = + CountableSetSpecifier::AllExcept(vec![port(1), port(u16::MAX)]); + assert_eq!(set.ranges(), vec![port(2)..=port(u16::MAX - 1)]); + + let set: CountableSetSpecifier = + CountableSetSpecifier::AllExcept(vec![port(1234), port(5678)]); + assert_eq!( + set.ranges(), + vec![ + port(1)..=port(1233), + port(1235)..=port(5677), + port(5679)..=port(65535) + ] + ); + + let set: CountableSetSpecifier = + CountableSetSpecifier::AllExcept(vec![port(1), port(1234), port(5678), port(u16::MAX)]); + assert_eq!( + set.ranges(), + vec![ + port(2)..=port(1233), + port(1235)..=port(5677), + port(5679)..=port(65534) + ] + ); + + let set: CountableSetSpecifier = CountableSetSpecifier::All; + assert_eq!(set.ranges(), vec![port(1)..=port(u16::MAX)]); + } } diff --git a/src/systemd/options.rs b/src/systemd/options.rs index b2a99e8..fbc1398 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -20,11 +20,21 @@ use crate::{ systemd::{KernelVersion, SystemdVersion}, }; +/// Callbacks to dynamically update an option to make it compatible with an action +#[derive(Debug)] +pub(crate) struct OptionUpdater { + /// Generate a new option effect compatible with the previously incompatible action + pub effect: fn(&OptionValueEffect, &ProgramAction) -> Option, + /// Generate the option value from the new effect + pub value: fn(&OptionValueEffect) -> OptionValue, +} + /// Systemd option with its possibles values, and their effect #[derive(Debug)] pub(crate) struct OptionDescription { pub name: &'static str, pub possible_values: Vec, + pub updater: Option, } impl fmt::Display for OptionDescription { @@ -862,6 +872,7 @@ pub(crate) fn build_options( })), }, ], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectHome= @@ -918,6 +929,7 @@ pub(crate) fn build_options( )), }, ], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateTmp= @@ -936,6 +948,7 @@ pub(crate) fn build_options( }), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateDevices= @@ -968,6 +981,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("raw-io")), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelTunables= @@ -1014,6 +1028,7 @@ pub(crate) fn build_options( .collect(), )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelModules= @@ -1034,6 +1049,7 @@ pub(crate) fn build_options( OptionValueEffect::DenySyscalls(DenySyscalls::Class("module")), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectKernelLogs= @@ -1053,6 +1069,7 @@ pub(crate) fn build_options( }), ])), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectControlGroups= @@ -1065,6 +1082,7 @@ pub(crate) fn build_options( exceptions: vec![], })), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectProc= @@ -1085,6 +1103,7 @@ pub(crate) fn build_options( regex::bytes::Regex::new("^/proc/[0-9]+(/|$)").unwrap(), ))), }], + updater: None, }); } @@ -1098,6 +1117,7 @@ pub(crate) fn build_options( ProgramAction::WriteExecuteMemoryMapping, )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies= @@ -1173,6 +1193,7 @@ pub(crate) fn build_options( .collect(), ), }], + updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1194,6 +1215,7 @@ pub(crate) fn build_options( }), )), }], + updater: None, }); } @@ -1234,6 +1256,59 @@ pub(crate) fn build_options( .collect(), ), }], + updater: Some(OptionUpdater { + effect: |e, a| { + let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(effect_na)) = e + else { + unreachable!(); + }; + let ProgramAction::NetworkActivity(NetworkActivity { + local_port: CountableSetSpecifier::One(local_port), + .. + }) = a + else { + unreachable!(); + }; + let mut new_eff_local_port = effect_na.local_port.clone(); + new_eff_local_port.remove(local_port); + Some(OptionValueEffect::DenyAction( + ProgramAction::NetworkActivity(NetworkActivity { + af: effect_na.af.clone(), + proto: effect_na.proto.clone(), + kind: effect_na.kind.clone(), + local_port: new_eff_local_port, + }), + )) + }, + value: |e| { + let OptionValueEffect::DenyAction(ProgramAction::NetworkActivity(denied_na)) = e + else { + unreachable!(); + }; + OptionValue::List { + values: denied_na + .af + .elements() + .iter() + .cartesian_product(denied_na.proto.elements()) + .cartesian_product(denied_na.local_port.ranges()) + .map(|((af, proto), port_range)| { + format!( + "{}:{}:{}-{}", + af, + proto, + port_range.start(), + port_range.end() + ) + }) + .collect(), + value_if_empty: None, + negation_prefix: false, + repeat_option: true, + mode: ListMode::BlackList, + } + }, + }), }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality= @@ -1248,6 +1323,7 @@ pub(crate) fn build_options( "personality", ))), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictRealtime= @@ -1259,6 +1335,7 @@ pub(crate) fn build_options( ProgramAction::SetRealtimeScheduler, )), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#ProtectClock= @@ -1271,6 +1348,7 @@ pub(crate) fn build_options( "clock", ))), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#CapabilityBoundingSet= @@ -1430,6 +1508,7 @@ pub(crate) fn build_options( }, desc: OptionEffect::Cumulative(cap_effects.into_iter().map(|(_c, e)| e).collect()), }], + updater: None, }); // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter= @@ -1464,6 +1543,7 @@ pub(crate) fn build_options( .collect(), ), }], + updater: None, }); if let HardeningMode::Aggressive = mode { @@ -1477,6 +1557,7 @@ pub(crate) fn build_options( value: OptionValue::String("native".to_owned()), desc: OptionEffect::None, }], + updater: None, }); } diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index 5843fdb..7ff53be 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -7,78 +7,161 @@ use crate::{ }, }; +use super::options::OptionUpdater; + impl OptionValueEffect { - fn compatible(&self, action: &ProgramAction, prev_actions: &[ProgramAction]) -> bool { + fn compatible( + &self, + action: &ProgramAction, + prev_actions: &[ProgramAction], + updater: Option<&OptionUpdater>, + ) -> ActionOptionEffectCompatibility { match self { - OptionValueEffect::DenyAction(denied) => match denied { - ProgramAction::NetworkActivity(denied) => { - if let ProgramAction::NetworkActivity(NetworkActivity { - af, - proto, - kind, - local_port, - }) = action - { - !denied.af.intersects(af) - || !denied.proto.intersects(proto) - || !denied.kind.intersects(kind) - || !denied.local_port.intersects(local_port) + OptionValueEffect::DenyAction(denied) => { + let compatible = match denied { + ProgramAction::NetworkActivity(denied) => { + if let ProgramAction::NetworkActivity(NetworkActivity { + af, + proto, + kind, + local_port, + }) = action + { + let af_match = denied.af.intersects(af); + let proto_match = denied.proto.intersects(proto); + let kind_match = denied.kind.intersects(kind); + let local_port_match = denied.local_port.intersects(local_port); + !af_match || !proto_match || !kind_match || !local_port_match + } else { + true + } + } + ProgramAction::WriteExecuteMemoryMapping + | ProgramAction::SetRealtimeScheduler + | ProgramAction::Wakeup + | ProgramAction::MknodSpecial + | ProgramAction::SetAlarm => action != denied, + ProgramAction::Syscalls(_) + | ProgramAction::Read(_) + | ProgramAction::Write(_) + | ProgramAction::Create(_) => unreachable!(), + }; + if compatible { + ActionOptionEffectCompatibility::Compatible + } else if let Some(updater) = updater { + if let Some(new_eff) = (updater.effect)(self, action) { + ActionOptionEffectCompatibility::CompatibleIfChanged( + ChangedOptionValueDescription { + value: (updater.value)(&new_eff), + effect: new_eff, + }, + ) } else { - true + ActionOptionEffectCompatibility::Incompatible } + } else { + ActionOptionEffectCompatibility::Incompatible } - ProgramAction::WriteExecuteMemoryMapping - | ProgramAction::SetRealtimeScheduler - | ProgramAction::Wakeup - | ProgramAction::MknodSpecial - | ProgramAction::SetAlarm => action != denied, - ProgramAction::Syscalls(_) - | ProgramAction::Read(_) - | ProgramAction::Write(_) - | ProgramAction::Create(_) => unreachable!(), - }, + } OptionValueEffect::DenyWrite(ro_paths) => match action { ProgramAction::Write(path_action) | ProgramAction::Create(path_action) => { - !ro_paths.matches(path_action) + ActionOptionEffectCompatibility::from(!ro_paths.matches(path_action)) } - _ => true, + _ => ActionOptionEffectCompatibility::Compatible, }, OptionValueEffect::Hide(hidden_paths) => { if let ProgramAction::Read(path_action) = action { - !hidden_paths.matches(path_action) - || prev_actions.contains(&ProgramAction::Create(path_action.clone())) + (!hidden_paths.matches(path_action) + || prev_actions.contains(&ProgramAction::Create(path_action.clone()))) + .into() } else { - true + ActionOptionEffectCompatibility::Compatible } } OptionValueEffect::DenySyscalls(denied) => { if let ProgramAction::Syscalls(syscalls) = action { let denied_syscalls = denied.syscalls(); let syscalls = syscalls.iter().map(String::as_str).collect(); - denied_syscalls.intersection(&syscalls).next().is_none() + denied_syscalls + .intersection(&syscalls) + .next() + .is_none() + .into() } else { - true + ActionOptionEffectCompatibility::Compatible } } - OptionValueEffect::Multiple(effects) => { - effects.iter().all(|e| e.compatible(action, prev_actions)) - } + OptionValueEffect::Multiple(effects) => effects + .iter() + .all(|e| match e.compatible(action, prev_actions, None) { + ActionOptionEffectCompatibility::Compatible => true, + ActionOptionEffectCompatibility::CompatibleIfChanged(_) => todo!(), + ActionOptionEffectCompatibility::Incompatible => false, + }) + .into(), } } } -pub(crate) fn actions_compatible(eff: &OptionValueEffect, actions: &[ProgramAction]) -> bool { +/// A systemd option value and its effect, altered from original +#[derive(Debug)] +pub(crate) struct ChangedOptionValueDescription { + pub value: OptionValue, + pub effect: OptionValueEffect, +} + +/// How compatible is an action with an option effect? +pub(crate) enum ActionOptionEffectCompatibility { + Compatible, + CompatibleIfChanged(ChangedOptionValueDescription), + Incompatible, +} + +impl From for ActionOptionEffectCompatibility { + fn from(value: bool) -> Self { + if value { + Self::Compatible + } else { + Self::Incompatible + } + } +} + +pub(crate) fn actions_compatible( + eff: &OptionValueEffect, + actions: &[ProgramAction], + updater: Option<&OptionUpdater>, +) -> ActionOptionEffectCompatibility { + let mut changed_desc: Option = None; for i in 0..actions.len() { - if !eff.compatible(&actions[i], &actions[..i]) { - log::debug!( - "Option effect {:?} is incompatible with {:?}", - eff, - actions[i] - ); - return false; + let cur_eff = changed_desc.as_ref().map_or(eff, |d| &d.effect); + match cur_eff.compatible(&actions[i], &actions[..i], updater) { + ActionOptionEffectCompatibility::Compatible => {} + ActionOptionEffectCompatibility::CompatibleIfChanged(new_desc) => { + log::debug!( + "Option effect {:?} is incompatible with {:?}, changing effect to {:?}", + cur_eff, + actions[i], + new_desc.effect + ); + changed_desc = Some(new_desc); + } + ActionOptionEffectCompatibility::Incompatible => { + log::debug!( + "Option effect {:?} is incompatible with {:?}", + cur_eff, + actions[i] + ); + return ActionOptionEffectCompatibility::Incompatible; + } } } - true + + if let Some(new_desc) = changed_desc { + ActionOptionEffectCompatibility::CompatibleIfChanged(new_desc) + } else { + ActionOptionEffectCompatibility::Compatible + } } pub(crate) fn resolve( @@ -99,12 +182,22 @@ pub(crate) fn resolve( break; } OptionEffect::Simple(effect) => { - if actions_compatible(effect, actions) { - candidates.push(OptionWithValue { - name: opt.name.to_owned(), - value: opt_value_desc.value.clone(), - }); - break; + match actions_compatible(effect, actions, opt.updater.as_ref()) { + ActionOptionEffectCompatibility::Compatible => { + candidates.push(OptionWithValue { + name: opt.name.to_owned(), + value: opt_value_desc.value.clone(), + }); + break; + } + ActionOptionEffectCompatibility::CompatibleIfChanged(opt_new_desc) => { + candidates.push(OptionWithValue { + name: opt.name.to_owned(), + value: opt_new_desc.value.clone(), + }); + break; + } + ActionOptionEffectCompatibility::Incompatible => {} } } OptionEffect::Cumulative(effects) => { @@ -118,14 +211,42 @@ pub(crate) fn resolve( } => { let mut compatible_opts = Vec::new(); debug_assert_eq!(values.len(), effects.len()); - for (optv, opte) in values.iter().zip(effects) { - let compatible = actions_compatible(opte, actions); + let mut cur_effects = effects.clone(); + for (optv, opte) in values.iter().zip(&mut cur_effects) { + let compatible = + actions_compatible(opte, actions, opt.updater.as_ref()); + let mut cur_opt_vals = vec![optv.to_owned()]; let enable_opt = match mode { - ListMode::WhiteList => !compatible, - ListMode::BlackList => compatible, + ListMode::WhiteList => matches!( + compatible, + ActionOptionEffectCompatibility::Incompatible + ), + ListMode::BlackList => match compatible { + ActionOptionEffectCompatibility::Compatible => true, + ActionOptionEffectCompatibility::CompatibleIfChanged( + nd, + ) => { + *opte = nd.effect; + match actions_compatible(opte, actions, None) { + ActionOptionEffectCompatibility::Compatible => { + cur_opt_vals = if let OptionValue::List { + values: new_vals, .. + } = nd.value { + new_vals + } else { + unreachable!(); + }; + true + }, + ActionOptionEffectCompatibility::CompatibleIfChanged(_) => unreachable!(), + ActionOptionEffectCompatibility::Incompatible => false, + } + } + ActionOptionEffectCompatibility::Incompatible => false, + }, }; if enable_opt { - compatible_opts.push(optv.to_string()); + compatible_opts.append(&mut cur_opt_vals); } } if !compatible_opts.is_empty() || value_if_empty.is_some() { diff --git a/tests/cl.rs b/tests/cl.rs index 7c7e6bc..dcd7af1 100644 --- a/tests/cl.rs +++ b/tests/cl.rs @@ -526,6 +526,41 @@ fn run_bind() { .stdout(predicate::str::contains("MemoryDenyWriteExecute=\n").not()) .stdout(predicate::str::contains("RestrictAddressFamilies=AF_INET\n").count(1)) .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").not()) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp:1-1233\n").not()) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp:1235-65535\n").not()) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)) + .stdout(predicate::str::contains("CapabilityBoundingSet=~CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN CAP_MKNOD CAP_NET_RAW CAP_PERFMON CAP_SYS_BOOT CAP_SYS_CHROOT CAP_SYS_MODULE CAP_SYS_NICE CAP_SYS_PACCT CAP_SYS_PTRACE CAP_SYS_TIME CAP_SYS_TTY_CONFIG CAP_SYSLOG CAP_WAKE_ALARM\n").count(1)); + + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "-f", "--", "python3", "-c", "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind((\"127.0.0.1\", 1234))"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(if env::current_exe().unwrap().starts_with("/tmp") { + predicate::str::contains("PrivateTmp=true\n").count(0) + } else { + predicate::str::contains("PrivateTmp=true\n").count(1) + }) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("MemoryDenyWriteExecute=\n").not()) + .stdout(predicate::str::contains("RestrictAddressFamilies=AF_INET\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").not()) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp:1-1233\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp:1235-65535\n").count(1)) .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1))