diff --git a/src/cl.rs b/src/cl.rs index 177bd35..c0c6a77 100644 --- a/src/cl.rs +++ b/src/cl.rs @@ -1,5 +1,7 @@ //! Command line interface +use std::path::PathBuf; + use clap::Parser; #[derive(Parser, Debug)] @@ -29,6 +31,17 @@ pub enum Action { /// How hard we should harden #[arg(short, long, default_value_t, value_enum)] mode: HardeningMode, + /// Generate profile data file to be merged with others instead of generating systemd options directly + #[arg(short, long, default_value = None)] + profile_data_path: Option, + }, + /// Merge profile data from previous runs to generate systemd options + MergeProfileData { + /// How hard we should harden + #[arg(short, long, default_value_t, value_enum)] + mode: HardeningMode, + /// Profile data paths + paths: Vec, }, /// Act on a systemd service unit #[clap(subcommand)] diff --git a/src/main.rs b/src/main.rs index 2740b06..c42b957 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,11 @@ fn main() -> anyhow::Result<()> { // Handle CL args match args.action { - cl::Action::Run { command, mode } => { + cl::Action::Run { + command, + mode, + profile_data_path, + } => { // Build supported systemd options let sd_opts = sd_options(&sd_version, &kernel_version, &mode)?; @@ -74,6 +78,7 @@ fn main() -> anyhow::Result<()> { // Report systemd::report_options(resolved_opts); } + cl::Action::MergeProfileData { mode, paths } => todo!(), cl::Action::Service(cl::ServiceAction::StartProfile { service, mode, diff --git a/src/systemd/service.rs b/src/systemd/service.rs index a20fcb0..cb0b407 100644 --- a/src/systemd/service.rs +++ b/src/systemd/service.rs @@ -61,29 +61,12 @@ impl Service { "Hardening config already exists at {harden_fragment_path:?} and may conflict with profiling" ); - // Check we have no ExecStartPre/ExecStartPost - // Currently we don't profile those, so we'd rather throw an explicit error let config_paths_bufs = self.config_paths()?; let config_paths = config_paths_bufs .iter() .map(|p| p.as_path()) .collect::>(); - if !Self::config_vals("ExecStartPre", &config_paths)?.is_empty() - || !Self::config_vals("ExecStartPost", &config_paths)?.is_empty() - { - anyhow::bail!("Services with ExecStartPre/ExecStartPost are not supported") - } - - // Read current service startup command log::info!("Located unit config file(s): {config_paths:?}"); - let cmd = Self::config_vals("ExecStart", &config_paths)? - .into_iter() - .at_most_one() - .map_err(|_| { - anyhow::anyhow!("Services with multiple ExecStart directives are not supported") - })? - .ok_or_else(|| anyhow::anyhow!("Unable to get service ExecStart command"))?; - log::info!("Startup command: {cmd}"); // Write new fragment fs::create_dir_all(fragment_path.parent().unwrap())?; @@ -104,17 +87,44 @@ impl Service { } // strace may slow down enough to risk reaching some service timeouts writeln!(fragment_file, "TimeoutStartSec=infinity")?; - writeln!(fragment_file, "ExecStart=")?; + + // Profile data dir + let profile_data_dir = PathBuf::from(format!("/{}_profile_data", env!("CARGO_PKG_NAME"))); writeln!( fragment_file, - "ExecStart={} run -m {} -- {}", - env::current_exe()? - .to_str() - .ok_or_else(|| anyhow::anyhow!("Unable to decode current executable path"))?, - mode, - cmd + "TemporaryFileSystem={}", + profile_data_dir.to_str().unwrap() )?; + let shh_bin = env::current_exe()? + .to_str() + .ok_or_else(|| anyhow::anyhow!("Unable to decode current executable path"))? + .to_string(); + + // Wrap all ExecStartXxx directives + let mut exec_start_idx = 1; + for exec_start_opt in ["ExecStartPre", "ExecStart", "ExecStartPost"] { + let exec_start_cmds = Self::config_vals(exec_start_opt, &config_paths)?; + if !exec_start_cmds.is_empty() { + writeln!(fragment_file, "{}=", exec_start_opt)?; + } + for cmd in exec_start_cmds { + let profile_data_path = profile_data_dir.join(format!("{:03}", exec_start_idx)); + exec_start_idx += 1; + writeln!( + fragment_file, + "{}={} run -m {} -p {} -- {}", + exec_start_opt, + shh_bin, + mode, + profile_data_path.to_str().unwrap(), + cmd + )?; + } + } + + // TODO ExecStopPost shh invocation that merges previous profiles + log::info!("Config fragment written in {fragment_path:?}"); Ok(()) }