use sysknife_types::RiskLevel;
pub mod apparmor;
pub mod apt;
pub mod apt_preferences;
pub mod auditd;
pub mod certbot;
pub mod cloudinit;
pub mod containers;
pub mod deployment;
pub mod distrobox;
pub mod fail2ban;
pub mod filesystem;
pub mod flatpak;
pub mod grub;
pub mod identity;
pub mod journald;
pub mod layering;
pub mod livepatch;
pub mod logging;
pub mod lvm;
pub mod mounts;
pub mod multipass;
pub mod netplan;
pub mod network;
pub mod package_repos;
pub mod pam;
pub mod ppa;
pub mod processes;
pub mod reboot;
pub mod release_upgrade;
pub mod resolvectl;
pub mod services;
pub mod snap;
pub mod ssh;
pub mod sudoers;
pub mod sysctl;
pub mod system_info;
pub mod toolbox;
pub mod ubuntu_pro;
pub mod ufw;
pub mod users;
pub(crate) mod validate;
pub const OBSERVER_MUTATING_ACTIONS: &[&str] = &["AptUpdate"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ActionMechanism {
Command {
program: &'static str,
args: Vec<String>,
},
FileScan {
path: String,
},
FileWrite {
path: String,
content: String,
},
FilePatch {
path: String,
search: String,
replace: String,
},
FileDelete {
path: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActionSpec {
pub action_name: &'static str,
pub mechanism: ActionMechanism,
pub risk_level: RiskLevel,
pub reboot_required: bool,
pub rollback_available: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ExclusiveResource {
System,
Dpkg,
Snap,
RpmOstree,
Flatpak,
}
impl ExclusiveResource {
pub fn label(self) -> &'static str {
match self {
Self::System => "the system (a reboot-required action)",
Self::Dpkg => "the dpkg/apt lock",
Self::Snap => "the snapd change queue",
Self::RpmOstree => "the rpm-ostree transaction lock",
Self::Flatpak => "the flatpak installation lock",
}
}
}
pub fn exclusive_resource(spec: &ActionSpec) -> Option<ExclusiveResource> {
let ActionMechanism::Command { program, args } = &spec.mechanism else {
return None;
};
std::iter::once(*program)
.chain(args.iter().map(String::as_str))
.flat_map(|token| {
token.split(|c: char| c.is_whitespace() || matches!(c, '&' | '|' | ';' | '(' | ')'))
})
.filter(|word| !word.is_empty())
.find_map(|token| {
let name = token.rsplit('/').next().unwrap_or(token);
match name {
"apt-get" | "apt" | "apt-mark" | "aptitude" | "dpkg" | "dpkg-reconfigure"
| "add-apt-repository" | "do-release-upgrade" | "unattended-upgrade"
| "apt-pin-edit" => Some(ExclusiveResource::Dpkg),
"snap" | "snap-install-hold" => Some(ExclusiveResource::Snap),
"rpm-ostree" | "ostree" => Some(ExclusiveResource::RpmOstree),
"flatpak" => Some(ExclusiveResource::Flatpak),
_ => None,
}
})
}
pub fn catalogue() -> Vec<(&'static str, Vec<ActionSpec>)> {
vec![
("Deployment (atomic host)", deployment::specs()),
("Package layering (rpm-ostree)", layering::specs()),
("Filesystem", filesystem::specs()),
("Flatpak", flatpak::specs()),
("Toolbox", toolbox::specs()),
("Services", services::specs()),
("Processes", processes::specs()),
("Journald", journald::specs()),
("Storage — LVM", lvm::specs()),
("Kernel parameters — sysctl", sysctl::specs()),
("Mounts & swap", mounts::specs()),
("Log management", logging::specs()),
("PAM password policy", pam::specs()),
("auditd", auditd::specs()),
("certbot / ACME", certbot::specs()),
("Scoped sudoers.d", sudoers::specs()),
("Network", network::specs()),
("resolvectl", resolvectl::specs()),
("Identity / time / locale", identity::specs()),
("Users & groups", users::specs()),
("SSH keys", ssh::specs()),
("Package repositories", package_repos::specs()),
("System info", system_info::specs()),
("Containers", containers::specs()),
("Reboot", reboot::specs()),
("AppArmor", apparmor::specs()),
("cloud-init", cloudinit::specs()),
("fail2ban", fail2ban::specs()),
("apt", apt::specs()),
("apt preferences / pinning", apt_preferences::specs()),
("PPA", ppa::specs()),
("snap", snap::specs()),
("ufw", ufw::specs()),
("netplan", netplan::specs()),
("distrobox", distrobox::specs()),
("GRUB", grub::specs()),
("Ubuntu release upgrade", release_upgrade::specs()),
("Ubuntu Pro", ubuntu_pro::specs()),
("Livepatch", livepatch::specs()),
("Multipass", multipass::specs()),
]
}
pub fn all_specs() -> Vec<ActionSpec> {
catalogue()
.into_iter()
.flat_map(|(_, specs)| specs)
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpecMeta {
pub risk_level: RiskLevel,
pub reboot_required: bool,
pub rollback_available: bool,
}
static SPEC_META: std::sync::LazyLock<std::collections::HashMap<&'static str, SpecMeta>> =
std::sync::LazyLock::new(|| {
all_specs()
.into_iter()
.map(|s| {
(
s.action_name,
SpecMeta {
risk_level: s.risk_level,
reboot_required: s.reboot_required,
rollback_available: s.rollback_available,
},
)
})
.collect()
});
pub const DISPATCHER_INTERNAL_ACTIONS: &[&str] = &["ListJobHistory"];
pub fn spec_meta(action_name: &str) -> Option<SpecMeta> {
SPEC_META.get(action_name).copied()
}
pub(crate) fn command_mechanism(
program: &'static str,
args: impl IntoIterator<Item = impl Into<String>>,
) -> ActionMechanism {
ActionMechanism::Command {
program,
args: args.into_iter().map(Into::into).collect(),
}
}
pub(crate) fn file_write_mechanism(
path: impl Into<String>,
content: impl Into<String>,
) -> ActionMechanism {
ActionMechanism::FileWrite {
path: path.into(),
content: content.into(),
}
}
pub(crate) fn file_patch_mechanism(
path: impl Into<String>,
search: impl Into<String>,
replace: impl Into<String>,
) -> ActionMechanism {
ActionMechanism::FilePatch {
path: path.into(),
search: search.into(),
replace: replace.into(),
}
}
pub(crate) fn file_scan_mechanism(path: impl Into<String>) -> ActionMechanism {
ActionMechanism::FileScan { path: path.into() }
}
pub(crate) fn file_delete_mechanism(path: impl Into<String>) -> ActionMechanism {
ActionMechanism::FileDelete { path: path.into() }
}