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
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum ActionMechanism {
47 Command {
48 program: &'static str,
49 args: Vec<String>,
50 },
51 FileScan {
52 path: String,
53 },
54 FileWrite {
55 path: String,
56 content: String,
57 },
58 FilePatch {
59 path: String,
60 search: String,
61 replace: String,
62 },
63 FileDelete {
64 path: String,
65 },
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct ActionSpec {
70 pub action_name: &'static str,
71 pub mechanism: ActionMechanism,
72 pub risk_level: RiskLevel,
73 pub reboot_required: bool,
74 pub rollback_available: bool,
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
86pub enum ExclusiveResource {
87 System,
91 Dpkg,
93 Snap,
95 RpmOstree,
97 Flatpak,
99}
100
101impl ExclusiveResource {
102 pub fn label(self) -> &'static str {
104 match self {
105 Self::System => "the system (a reboot-required action)",
106 Self::Dpkg => "the dpkg/apt lock",
107 Self::Snap => "the snapd change queue",
108 Self::RpmOstree => "the rpm-ostree transaction lock",
109 Self::Flatpak => "the flatpak installation lock",
110 }
111 }
112}
113
114pub fn exclusive_resource(spec: &ActionSpec) -> Option<ExclusiveResource> {
126 let ActionMechanism::Command { program, args } = &spec.mechanism else {
127 return None;
128 };
129 std::iter::once(*program)
130 .chain(args.iter().map(String::as_str))
131 .flat_map(|token| {
137 token.split(|c: char| c.is_whitespace() || matches!(c, '&' | '|' | ';' | '(' | ')'))
138 })
139 .filter(|word| !word.is_empty())
140 .find_map(|token| {
141 let name = token.rsplit('/').next().unwrap_or(token);
143 match name {
144 "apt-get"
145 | "apt"
146 | "apt-mark"
147 | "aptitude"
148 | "dpkg"
149 | "dpkg-reconfigure"
150 | "add-apt-repository"
151 | "do-release-upgrade"
152 | "unattended-upgrade"
153 | "sysknife-apt-pin-edit"
154 | "apt-pin-edit" => Some(ExclusiveResource::Dpkg),
155 "snap" => 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}