1use super::{command_mechanism, ActionSpec};
23use sysknife_types::RiskLevel;
24
25const APT_GET: &str = "apt-get";
31
32const DEBIAN_FRONTEND_VALUE: &str = "DEBIAN_FRONTEND=noninteractive";
35
36const NEEDRESTART_MODE_VALUE: &str = "NEEDRESTART_MODE=a";
40
41pub fn specs() -> Vec<ActionSpec> {
48 vec![
49 apt_update(),
50 apt_upgrade(),
51 apt_install("curl"),
52 apt_remove("curl"),
53 apt_purge("curl"),
54 apt_autoremove(),
55 apt_hold("curl"),
56 apt_unhold("curl"),
57 apt_search("curl"),
58 apt_list_installed(),
59 apt_show("curl"),
60 apt_list_upgradable(),
61 apt_history_list(),
62 configure_unattended_upgrades(true),
63 ]
64}
65
66const UNATTENDED_UPGRADES_HELPER: &str = "/usr/lib/sysknife/unattended-upgrades-edit";
70
71pub fn configure_unattended_upgrades(enabled: bool) -> ActionSpec {
78 let flag = if enabled { "--enable" } else { "--disable" };
79 ActionSpec {
80 action_name: "ConfigureUnattendedUpgrades",
81 mechanism: command_mechanism("sudo", [UNATTENDED_UPGRADES_HELPER, flag]),
82 risk_level: RiskLevel::High,
83 reboot_required: false,
84 rollback_available: false,
85 }
86}
87
88pub fn apt_update() -> ActionSpec {
97 ActionSpec {
98 action_name: "AptUpdate",
99 mechanism: command_mechanism(
100 "sudo",
101 [
102 "env",
103 DEBIAN_FRONTEND_VALUE,
104 NEEDRESTART_MODE_VALUE,
105 APT_GET,
106 "update",
107 ],
108 ),
109 risk_level: RiskLevel::Low,
110 reboot_required: false,
111 rollback_available: false,
112 }
113}
114
115pub fn apt_upgrade() -> ActionSpec {
120 ActionSpec {
121 action_name: "AptUpgrade",
122 mechanism: command_mechanism(
123 "sudo",
124 [
125 "env",
126 DEBIAN_FRONTEND_VALUE,
127 NEEDRESTART_MODE_VALUE,
128 APT_GET,
129 "dist-upgrade",
130 "-y",
131 ],
132 ),
133 risk_level: RiskLevel::High,
134 reboot_required: false,
135 rollback_available: false,
136 }
137}
138
139pub fn apt_install(package: &str) -> ActionSpec {
144 ActionSpec {
145 action_name: "AptInstall",
146 mechanism: command_mechanism(
147 "sudo",
148 [
149 "env",
150 DEBIAN_FRONTEND_VALUE,
151 NEEDRESTART_MODE_VALUE,
152 APT_GET,
153 "install",
154 "-y",
155 package,
156 ],
157 ),
158 risk_level: RiskLevel::Medium,
159 reboot_required: false,
160 rollback_available: false,
161 }
162}
163
164pub fn apt_remove(package: &str) -> ActionSpec {
169 ActionSpec {
170 action_name: "AptRemove",
171 mechanism: command_mechanism(
172 "sudo",
173 [
174 "env",
175 DEBIAN_FRONTEND_VALUE,
176 NEEDRESTART_MODE_VALUE,
177 APT_GET,
178 "remove",
179 "-y",
180 package,
181 ],
182 ),
183 risk_level: RiskLevel::Medium,
184 reboot_required: false,
185 rollback_available: false,
186 }
187}
188
189pub fn apt_purge(package: &str) -> ActionSpec {
194 ActionSpec {
195 action_name: "AptPurge",
196 mechanism: command_mechanism(
197 "sudo",
198 [
199 "env",
200 DEBIAN_FRONTEND_VALUE,
201 NEEDRESTART_MODE_VALUE,
202 APT_GET,
203 "purge",
204 "-y",
205 package,
206 ],
207 ),
208 risk_level: RiskLevel::Medium,
209 reboot_required: false,
210 rollback_available: false,
211 }
212}
213
214pub fn apt_autoremove() -> ActionSpec {
219 ActionSpec {
220 action_name: "AptAutoremove",
221 mechanism: command_mechanism(
222 "sudo",
223 [
224 "env",
225 DEBIAN_FRONTEND_VALUE,
226 NEEDRESTART_MODE_VALUE,
227 APT_GET,
228 "autoremove",
229 "-y",
230 ],
231 ),
232 risk_level: RiskLevel::Medium,
233 reboot_required: false,
234 rollback_available: false,
235 }
236}
237
238pub fn apt_hold(package: &str) -> ActionSpec {
243 ActionSpec {
244 action_name: "AptHold",
245 mechanism: command_mechanism("sudo", ["apt-mark", "hold", package]),
246 risk_level: RiskLevel::Medium,
247 reboot_required: false,
248 rollback_available: false,
249 }
250}
251
252pub fn apt_unhold(package: &str) -> ActionSpec {
256 ActionSpec {
257 action_name: "AptUnhold",
258 mechanism: command_mechanism("sudo", ["apt-mark", "unhold", package]),
259 risk_level: RiskLevel::Medium,
260 reboot_required: false,
261 rollback_available: false,
262 }
263}
264
265pub fn apt_search(term: &str) -> ActionSpec {
269 ActionSpec {
270 action_name: "AptSearch",
271 mechanism: command_mechanism("apt-cache", ["search", term]),
272 risk_level: RiskLevel::Low,
273 reboot_required: false,
274 rollback_available: false,
275 }
276}
277
278pub fn apt_list_installed() -> ActionSpec {
282 ActionSpec {
283 action_name: "AptListInstalled",
284 mechanism: command_mechanism("dpkg", ["-l"]),
285 risk_level: RiskLevel::Low,
286 reboot_required: false,
287 rollback_available: false,
288 }
289}
290
291pub fn apt_show(package: &str) -> ActionSpec {
295 ActionSpec {
296 action_name: "AptShow",
297 mechanism: command_mechanism("apt-cache", ["show", package]),
298 risk_level: RiskLevel::Low,
299 reboot_required: false,
300 rollback_available: false,
301 }
302}
303
304pub fn apt_list_upgradable() -> ActionSpec {
312 ActionSpec {
313 action_name: "AptListUpgradable",
314 mechanism: command_mechanism("bash", ["-c", "apt list --upgradable 2>/dev/null"]),
315 risk_level: RiskLevel::Low,
316 reboot_required: false,
317 rollback_available: false,
318 }
319}
320
321pub fn apt_history_list() -> ActionSpec {
327 ActionSpec {
328 action_name: "AptHistoryList",
329 mechanism: command_mechanism(
330 "bash",
331 [
332 "-c",
333 "grep -A 4 '^Start-Date' /var/log/apt/history.log | tail -n 80",
334 ],
335 ),
336 risk_level: RiskLevel::Low,
337 reboot_required: false,
338 rollback_available: false,
339 }
340}
341
342#[cfg(test)]
347mod tests {
348 use super::*;
349 use crate::actions::ActionMechanism;
350
351 fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<&str>) {
352 match &spec.mechanism {
353 ActionMechanism::Command { program, args } => {
354 (*program, args.iter().map(String::as_str).collect())
355 }
356 _ => panic!("expected Command mechanism"),
357 }
358 }
359
360 #[test]
363 fn apt_update_action_name() {
364 assert_eq!(apt_update().action_name, "AptUpdate");
365 }
366
367 #[test]
368 fn apt_update_argv_correct() {
369 let spec = apt_update();
370 let (prog, args) = extract_args(&spec);
371 assert_eq!(prog, "sudo");
372 assert!(args.contains(&"apt-get"));
373 assert!(args.contains(&"update"));
374 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
375 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
376 }
377
378 #[test]
379 fn apt_update_risk_is_low() {
380 assert_eq!(apt_update().risk_level, RiskLevel::Low);
381 }
382
383 #[test]
384 fn apt_update_no_reboot_no_rollback() {
385 let spec = apt_update();
386 assert!(!spec.reboot_required);
387 assert!(!spec.rollback_available);
388 }
389
390 #[test]
393 fn apt_upgrade_action_name() {
394 assert_eq!(apt_upgrade().action_name, "AptUpgrade");
395 }
396
397 #[test]
398 fn apt_upgrade_argv_correct() {
399 let spec = apt_upgrade();
400 let (prog, args) = extract_args(&spec);
401 assert_eq!(prog, "sudo");
402 assert!(args.contains(&"apt-get"));
403 assert!(args.contains(&"dist-upgrade"));
404 assert!(args.contains(&"-y"));
405 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
406 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
407 }
408
409 #[test]
410 fn apt_upgrade_risk_is_high() {
411 assert_eq!(apt_upgrade().risk_level, RiskLevel::High);
412 }
413
414 #[test]
417 fn apt_install_action_name() {
418 assert_eq!(apt_install("vim").action_name, "AptInstall");
419 }
420
421 #[test]
422 fn apt_install_includes_package_name() {
423 let spec = apt_install("vim");
424 let (_, args) = extract_args(&spec);
425 assert!(args.contains(&"vim"));
426 assert!(args.contains(&"install"));
427 assert!(args.contains(&"-y"));
428 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
429 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
430 }
431
432 #[test]
433 fn apt_install_risk_is_medium() {
434 assert_eq!(apt_install("vim").risk_level, RiskLevel::Medium);
435 }
436
437 #[test]
440 fn apt_remove_action_name() {
441 assert_eq!(apt_remove("vim").action_name, "AptRemove");
442 }
443
444 #[test]
445 fn apt_remove_argv_contains_remove_not_purge() {
446 let spec = apt_remove("vim");
447 let (_, args) = extract_args(&spec);
448 assert!(args.contains(&"remove"));
449 assert!(!args.contains(&"purge"));
451 assert!(args.contains(&"vim"));
452 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
453 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
454 }
455
456 #[test]
457 fn apt_remove_risk_is_medium() {
458 assert_eq!(apt_remove("vim").risk_level, RiskLevel::Medium);
459 }
460
461 #[test]
464 fn apt_purge_action_name() {
465 assert_eq!(apt_purge("vim").action_name, "AptPurge");
466 }
467
468 #[test]
469 fn apt_purge_argv_uses_purge_subcommand() {
470 let spec = apt_purge("vim");
471 let (_, args) = extract_args(&spec);
472 assert!(args.contains(&"purge"));
473 assert!(args.contains(&"vim"));
474 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
475 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
476 }
477
478 #[test]
479 fn apt_purge_risk_is_medium() {
480 assert_eq!(apt_purge("vim").risk_level, RiskLevel::Medium);
481 }
482
483 #[test]
486 fn apt_autoremove_action_name() {
487 assert_eq!(apt_autoremove().action_name, "AptAutoremove");
488 }
489
490 #[test]
491 fn apt_autoremove_argv_correct() {
492 let spec = apt_autoremove();
493 let (prog, args) = extract_args(&spec);
494 assert_eq!(prog, "sudo");
495 assert!(args.contains(&"autoremove"));
496 assert!(args.contains(&"-y"));
497 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
498 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
499 }
500
501 #[test]
502 fn apt_autoremove_risk_is_medium() {
503 assert_eq!(apt_autoremove().risk_level, RiskLevel::Medium);
504 }
505
506 #[test]
509 fn apt_hold_action_name() {
510 assert_eq!(apt_hold("nginx").action_name, "AptHold");
511 }
512
513 #[test]
514 fn apt_hold_uses_apt_mark_hold() {
515 let spec = apt_hold("nginx");
516 let (prog, args) = extract_args(&spec);
517 assert_eq!(prog, "sudo");
518 assert!(args.contains(&"apt-mark"));
519 assert!(args.contains(&"hold"));
520 assert!(args.contains(&"nginx"));
521 }
522
523 #[test]
524 fn apt_hold_risk_is_medium() {
525 assert_eq!(apt_hold("nginx").risk_level, RiskLevel::Medium);
526 }
527
528 #[test]
531 fn apt_unhold_action_name() {
532 assert_eq!(apt_unhold("nginx").action_name, "AptUnhold");
533 }
534
535 #[test]
536 fn apt_unhold_uses_apt_mark_unhold() {
537 let spec = apt_unhold("nginx");
538 let (_, args) = extract_args(&spec);
539 assert!(args.contains(&"apt-mark"));
540 assert!(args.contains(&"unhold"));
541 assert!(args.contains(&"nginx"));
542 }
543
544 #[test]
545 fn apt_unhold_risk_is_medium() {
546 assert_eq!(apt_unhold("nginx").risk_level, RiskLevel::Medium);
547 }
548
549 #[test]
552 fn apt_search_action_name() {
553 assert_eq!(apt_search("docker").action_name, "AptSearch");
554 }
555
556 #[test]
557 fn apt_search_uses_apt_cache_search() {
558 let spec = apt_search("docker");
559 let (prog, args) = extract_args(&spec);
560 assert_eq!(prog, "apt-cache");
561 assert!(args.contains(&"search"));
562 assert!(args.contains(&"docker"));
563 }
564
565 #[test]
566 fn apt_search_risk_is_low() {
567 assert_eq!(apt_search("docker").risk_level, RiskLevel::Low);
568 }
569
570 #[test]
571 fn apt_search_no_sudo() {
572 let spec = apt_search("docker");
574 let (prog, _) = extract_args(&spec);
575 assert_ne!(prog, "sudo");
576 }
577
578 #[test]
581 fn apt_list_installed_action_name() {
582 assert_eq!(apt_list_installed().action_name, "AptListInstalled");
583 }
584
585 #[test]
586 fn apt_list_installed_uses_dpkg() {
587 let spec = apt_list_installed();
588 let (prog, args) = extract_args(&spec);
589 assert_eq!(prog, "dpkg");
590 assert!(args.contains(&"-l"));
591 }
592
593 #[test]
594 fn apt_list_installed_risk_is_low() {
595 assert_eq!(apt_list_installed().risk_level, RiskLevel::Low);
596 }
597
598 #[test]
601 fn apt_show_action_name() {
602 assert_eq!(apt_show("openssh-server").action_name, "AptShow");
603 }
604
605 #[test]
606 fn apt_show_uses_apt_cache_show() {
607 let spec = apt_show("openssh-server");
608 let (prog, args) = extract_args(&spec);
609 assert_eq!(prog, "apt-cache");
610 assert!(args.contains(&"show"));
611 assert!(args.contains(&"openssh-server"));
612 }
613
614 #[test]
615 fn apt_show_risk_is_low() {
616 assert_eq!(apt_show("openssh-server").risk_level, RiskLevel::Low);
617 }
618
619 #[test]
622 fn apt_list_upgradable_action_name() {
623 assert_eq!(apt_list_upgradable().action_name, "AptListUpgradable");
624 }
625
626 #[test]
627 fn apt_list_upgradable_uses_bash_and_apt_list() {
628 let spec = apt_list_upgradable();
629 match &spec.mechanism {
630 ActionMechanism::Command { program, args } => {
631 assert_eq!(*program, "bash");
632 let joined = args.join(" ");
633 assert!(joined.contains("apt list --upgradable"), "argv: {joined}");
634 }
635 _ => panic!("expected Command mechanism"),
636 }
637 }
638
639 #[test]
640 fn apt_list_upgradable_risk_is_low() {
641 assert_eq!(apt_list_upgradable().risk_level, RiskLevel::Low);
642 }
643
644 #[test]
647 fn apt_history_list_action_name() {
648 assert_eq!(apt_history_list().action_name, "AptHistoryList");
649 }
650
651 #[test]
652 fn apt_history_list_uses_bash_and_grep() {
653 let spec = apt_history_list();
654 match &spec.mechanism {
655 ActionMechanism::Command { program, args } => {
656 assert_eq!(*program, "bash");
657 let joined = args.join(" ");
658 assert!(
659 joined.contains("/var/log/apt/history.log"),
660 "argv: {joined}"
661 );
662 }
663 _ => panic!("expected Command mechanism"),
664 }
665 }
666
667 #[test]
668 fn apt_history_list_risk_is_low() {
669 assert_eq!(apt_history_list().risk_level, RiskLevel::Low);
670 }
671
672 #[test]
675 fn specs_covers_all_action_names() {
676 let expected_names = [
677 "AptUpdate",
678 "AptUpgrade",
679 "AptInstall",
680 "AptRemove",
681 "AptPurge",
682 "AptAutoremove",
683 "AptHold",
684 "AptUnhold",
685 "AptSearch",
686 "AptListInstalled",
687 "AptShow",
688 "AptListUpgradable",
689 "AptHistoryList",
690 ];
691 let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
692 for name in &expected_names {
693 assert!(
694 spec_names.contains(name),
695 "specs() is missing action {name}"
696 );
697 }
698 }
699
700 #[test]
703 fn mutating_apt_commands_carry_env_vars() {
704 let mutating = [
705 apt_upgrade(),
706 apt_install("vim"),
707 apt_remove("vim"),
708 apt_purge("vim"),
709 apt_autoremove(),
710 ];
711 for spec in &mutating {
712 let (_, args) = extract_args(spec);
713 assert!(
714 args.contains(&DEBIAN_FRONTEND_VALUE),
715 "{} missing DEBIAN_FRONTEND",
716 spec.action_name
717 );
718 assert!(
719 args.contains(&NEEDRESTART_MODE_VALUE),
720 "{} missing NEEDRESTART_MODE",
721 spec.action_name
722 );
723 }
724 }
725}