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"
153 | "apt"
154 | "apt-mark"
155 | "aptitude"
156 | "dpkg"
157 | "dpkg-reconfigure"
158 | "add-apt-repository"
159 | "do-release-upgrade"
160 | "unattended-upgrade"
161 | "sysknife-apt-pin-edit"
162 | "apt-pin-edit" => Some(ExclusiveResource::Dpkg),
163 "snap" => Some(ExclusiveResource::Snap),
164 "rpm-ostree" | "ostree" => Some(ExclusiveResource::RpmOstree),
165 "flatpak" => Some(ExclusiveResource::Flatpak),
166 _ => None,
167 }
168 })
169}
170
171pub fn catalogue() -> Vec<(&'static str, Vec<ActionSpec>)> {
180 vec![
181 ("Deployment (atomic host)", deployment::specs()),
182 ("Package layering (rpm-ostree)", layering::specs()),
183 ("Filesystem", filesystem::specs()),
184 ("Flatpak", flatpak::specs()),
185 ("Toolbox", toolbox::specs()),
186 ("Services", services::specs()),
187 ("Processes", processes::specs()),
188 ("Journald", journald::specs()),
189 ("Storage — LVM", lvm::specs()),
190 ("Kernel parameters — sysctl", sysctl::specs()),
191 ("Mounts & swap", mounts::specs()),
192 ("Log management", logging::specs()),
193 ("PAM password policy", pam::specs()),
194 ("auditd", auditd::specs()),
195 ("certbot / ACME", certbot::specs()),
196 ("Scoped sudoers.d", sudoers::specs()),
197 ("Network", network::specs()),
198 ("resolvectl", resolvectl::specs()),
199 ("Identity / time / locale", identity::specs()),
200 ("Users & groups", users::specs()),
201 ("SSH keys", ssh::specs()),
202 ("Package repositories", package_repos::specs()),
203 ("System info", system_info::specs()),
204 ("Containers", containers::specs()),
205 ("Reboot", reboot::specs()),
206 ("AppArmor", apparmor::specs()),
207 ("cloud-init", cloudinit::specs()),
208 ("fail2ban", fail2ban::specs()),
209 ("apt", apt::specs()),
210 ("apt preferences / pinning", apt_preferences::specs()),
211 ("PPA", ppa::specs()),
212 ("snap", snap::specs()),
213 ("ufw", ufw::specs()),
214 ("netplan", netplan::specs()),
215 ("distrobox", distrobox::specs()),
216 ("GRUB", grub::specs()),
217 ("Ubuntu release upgrade", release_upgrade::specs()),
218 ("Ubuntu Pro", ubuntu_pro::specs()),
219 ("Livepatch", livepatch::specs()),
220 ("Multipass", multipass::specs()),
221 ]
222}
223
224pub fn all_specs() -> Vec<ActionSpec> {
226 catalogue()
227 .into_iter()
228 .flat_map(|(_, specs)| specs)
229 .collect()
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
236pub struct SpecMeta {
237 pub risk_level: RiskLevel,
238 pub reboot_required: bool,
239 pub rollback_available: bool,
240}
241
242static SPEC_META: std::sync::LazyLock<std::collections::HashMap<&'static str, SpecMeta>> =
243 std::sync::LazyLock::new(|| {
244 all_specs()
245 .into_iter()
246 .map(|s| {
247 (
248 s.action_name,
249 SpecMeta {
250 risk_level: s.risk_level,
251 reboot_required: s.reboot_required,
252 rollback_available: s.rollback_available,
253 },
254 )
255 })
256 .collect()
257 });
258
259pub fn spec_meta(action_name: &str) -> Option<SpecMeta> {
262 SPEC_META.get(action_name).copied()
263}
264
265pub(crate) fn command_mechanism(
266 program: &'static str,
267 args: impl IntoIterator<Item = impl Into<String>>,
268) -> ActionMechanism {
269 ActionMechanism::Command {
270 program,
271 args: args.into_iter().map(Into::into).collect(),
272 }
273}
274
275pub(crate) fn file_write_mechanism(
276 path: impl Into<String>,
277 content: impl Into<String>,
278) -> ActionMechanism {
279 ActionMechanism::FileWrite {
280 path: path.into(),
281 content: content.into(),
282 }
283}
284
285pub(crate) fn file_patch_mechanism(
286 path: impl Into<String>,
287 search: impl Into<String>,
288 replace: impl Into<String>,
289) -> ActionMechanism {
290 ActionMechanism::FilePatch {
291 path: path.into(),
292 search: search.into(),
293 replace: replace.into(),
294 }
295}
296
297pub(crate) fn file_scan_mechanism(path: impl Into<String>) -> ActionMechanism {
298 ActionMechanism::FileScan { path: path.into() }
299}
300
301pub(crate) fn file_delete_mechanism(path: impl Into<String>) -> ActionMechanism {
302 ActionMechanism::FileDelete { path: path.into() }
303}