1use sysknife_types::RiskLevel;
2
3pub mod apparmor;
4pub mod apt;
5pub mod apt_preferences;
6pub mod auditd;
7pub mod certbot;
8pub mod cloudinit;
9pub mod containers;
10pub mod deployment;
11pub mod distrobox;
12pub mod fail2ban;
13pub mod filesystem;
14pub mod flatpak;
15pub mod grub;
16pub mod identity;
17pub mod journald;
18pub mod layering;
19pub mod livepatch;
20pub mod logging;
21pub mod lvm;
22pub mod mounts;
23pub mod multipass;
24pub mod netplan;
25pub mod network;
26pub mod package_repos;
27pub mod pam;
28pub mod ppa;
29pub mod processes;
30pub mod reboot;
31pub mod release_upgrade;
32pub mod resolvectl;
33pub mod services;
34pub mod snap;
35pub mod ssh;
36pub mod sudoers;
37pub mod sysctl;
38pub mod system_info;
39pub mod toolbox;
40pub mod ubuntu_pro;
41pub mod ufw;
42pub mod users;
43pub(crate) mod validate;
44
45pub const OBSERVER_MUTATING_ACTIONS: &[&str] = &["AptUpdate"];
52
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub enum ActionMechanism {
55 Command {
56 program: &'static str,
57 args: Vec<String>,
58 },
59 FileScan {
60 path: String,
61 },
62 FileWrite {
63 path: String,
64 content: String,
65 },
66 FilePatch {
67 path: String,
68 search: String,
69 replace: String,
70 },
71 FileDelete {
72 path: String,
73 },
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct ActionSpec {
78 pub action_name: &'static str,
79 pub mechanism: ActionMechanism,
80 pub risk_level: RiskLevel,
81 pub reboot_required: bool,
82 pub rollback_available: bool,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
94pub enum ExclusiveResource {
95 System,
99 Dpkg,
101 Snap,
103 RpmOstree,
105 Flatpak,
107}
108
109impl ExclusiveResource {
110 pub fn label(self) -> &'static str {
112 match self {
113 Self::System => "the system (a reboot-required action)",
114 Self::Dpkg => "the dpkg/apt lock",
115 Self::Snap => "the snapd change queue",
116 Self::RpmOstree => "the rpm-ostree transaction lock",
117 Self::Flatpak => "the flatpak installation lock",
118 }
119 }
120}
121
122pub fn exclusive_resource(spec: &ActionSpec) -> Option<ExclusiveResource> {
134 let ActionMechanism::Command { program, args } = &spec.mechanism else {
135 return None;
136 };
137 std::iter::once(*program)
138 .chain(args.iter().map(String::as_str))
139 .flat_map(|token| {
145 token.split(|c: char| c.is_whitespace() || matches!(c, '&' | '|' | ';' | '(' | ')'))
146 })
147 .filter(|word| !word.is_empty())
148 .find_map(|token| {
149 let name = token.rsplit('/').next().unwrap_or(token);
151 match name {
152 "apt-get" | "apt" | "apt-mark" | "aptitude" | "dpkg" | "dpkg-reconfigure"
153 | "add-apt-repository" | "do-release-upgrade" | "unattended-upgrade"
154 | "apt-pin-edit" => Some(ExclusiveResource::Dpkg),
155 "snap" | "snap-install-hold" => Some(ExclusiveResource::Snap),
156 "rpm-ostree" | "ostree" => Some(ExclusiveResource::RpmOstree),
157 "flatpak" => Some(ExclusiveResource::Flatpak),
158 _ => None,
159 }
160 })
161}
162
163pub fn catalogue() -> Vec<(&'static str, Vec<ActionSpec>)> {
172 vec![
173 ("Deployment (atomic host)", deployment::specs()),
174 ("Package layering (rpm-ostree)", layering::specs()),
175 ("Filesystem", filesystem::specs()),
176 ("Flatpak", flatpak::specs()),
177 ("Toolbox", toolbox::specs()),
178 ("Services", services::specs()),
179 ("Processes", processes::specs()),
180 ("Journald", journald::specs()),
181 ("Storage — LVM", lvm::specs()),
182 ("Kernel parameters — sysctl", sysctl::specs()),
183 ("Mounts & swap", mounts::specs()),
184 ("Log management", logging::specs()),
185 ("PAM password policy", pam::specs()),
186 ("auditd", auditd::specs()),
187 ("certbot / ACME", certbot::specs()),
188 ("Scoped sudoers.d", sudoers::specs()),
189 ("Network", network::specs()),
190 ("resolvectl", resolvectl::specs()),
191 ("Identity / time / locale", identity::specs()),
192 ("Users & groups", users::specs()),
193 ("SSH keys", ssh::specs()),
194 ("Package repositories", package_repos::specs()),
195 ("System info", system_info::specs()),
196 ("Containers", containers::specs()),
197 ("Reboot", reboot::specs()),
198 ("AppArmor", apparmor::specs()),
199 ("cloud-init", cloudinit::specs()),
200 ("fail2ban", fail2ban::specs()),
201 ("apt", apt::specs()),
202 ("apt preferences / pinning", apt_preferences::specs()),
203 ("PPA", ppa::specs()),
204 ("snap", snap::specs()),
205 ("ufw", ufw::specs()),
206 ("netplan", netplan::specs()),
207 ("distrobox", distrobox::specs()),
208 ("GRUB", grub::specs()),
209 ("Ubuntu release upgrade", release_upgrade::specs()),
210 ("Ubuntu Pro", ubuntu_pro::specs()),
211 ("Livepatch", livepatch::specs()),
212 ("Multipass", multipass::specs()),
213 ]
214}
215
216pub fn all_specs() -> Vec<ActionSpec> {
218 catalogue()
219 .into_iter()
220 .flat_map(|(_, specs)| specs)
221 .collect()
222}
223
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228pub struct SpecMeta {
229 pub risk_level: RiskLevel,
230 pub reboot_required: bool,
231 pub rollback_available: bool,
232}
233
234static SPEC_META: std::sync::LazyLock<std::collections::HashMap<&'static str, SpecMeta>> =
235 std::sync::LazyLock::new(|| {
236 all_specs()
237 .into_iter()
238 .map(|s| {
239 (
240 s.action_name,
241 SpecMeta {
242 risk_level: s.risk_level,
243 reboot_required: s.reboot_required,
244 rollback_available: s.rollback_available,
245 },
246 )
247 })
248 .collect()
249 });
250
251pub fn spec_meta(action_name: &str) -> Option<SpecMeta> {
254 SPEC_META.get(action_name).copied()
255}
256
257pub(crate) fn command_mechanism(
258 program: &'static str,
259 args: impl IntoIterator<Item = impl Into<String>>,
260) -> ActionMechanism {
261 ActionMechanism::Command {
262 program,
263 args: args.into_iter().map(Into::into).collect(),
264 }
265}
266
267pub(crate) fn file_write_mechanism(
268 path: impl Into<String>,
269 content: impl Into<String>,
270) -> ActionMechanism {
271 ActionMechanism::FileWrite {
272 path: path.into(),
273 content: content.into(),
274 }
275}
276
277pub(crate) fn file_patch_mechanism(
278 path: impl Into<String>,
279 search: impl Into<String>,
280 replace: impl Into<String>,
281) -> ActionMechanism {
282 ActionMechanism::FilePatch {
283 path: path.into(),
284 search: search.into(),
285 replace: replace.into(),
286 }
287}
288
289pub(crate) fn file_scan_mechanism(path: impl Into<String>) -> ActionMechanism {
290 ActionMechanism::FileScan { path: path.into() }
291}
292
293pub(crate) fn file_delete_mechanism(path: impl Into<String>) -> ActionMechanism {
294 ActionMechanism::FileDelete { path: path.into() }
295}