1use super::{command_mechanism, ActionSpec};
30use sysknife_types::RiskLevel;
31
32const APT_GET: &str = "/usr/bin/apt-get";
51
52const DEBIAN_FRONTEND_VALUE: &str = "DEBIAN_FRONTEND=noninteractive";
55
56const NEEDRESTART_MODE_VALUE: &str = "NEEDRESTART_MODE=a";
60
61pub fn specs() -> Vec<ActionSpec> {
68 vec![
69 apt_update(),
70 apt_upgrade(),
71 apt_install("curl"),
72 apt_remove("curl"),
73 apt_purge("curl"),
74 apt_autoremove(),
75 apt_hold("curl"),
76 apt_unhold("curl"),
77 apt_search("curl"),
78 apt_list_installed(),
79 apt_show("curl"),
80 apt_list_upgradable(),
81 apt_history_list(),
82 configure_unattended_upgrades(true),
83 ]
84}
85
86const UNATTENDED_UPGRADES_HELPER: &str = "/usr/lib/sysknife/unattended-upgrades-edit";
90
91pub fn configure_unattended_upgrades(enabled: bool) -> ActionSpec {
98 let flag = if enabled { "--enable" } else { "--disable" };
99 ActionSpec {
100 action_name: "ConfigureUnattendedUpgrades",
101 mechanism: command_mechanism("sudo", [UNATTENDED_UPGRADES_HELPER, flag]),
102 risk_level: RiskLevel::High,
103 reboot_required: false,
104 rollback_available: false,
105 }
106}
107
108pub fn apt_update() -> ActionSpec {
117 ActionSpec {
118 action_name: "AptUpdate",
119 mechanism: command_mechanism(
120 "sudo",
121 [
122 "env",
123 DEBIAN_FRONTEND_VALUE,
124 NEEDRESTART_MODE_VALUE,
125 APT_GET,
126 "update",
127 ],
128 ),
129 risk_level: RiskLevel::Low,
130 reboot_required: false,
131 rollback_available: false,
132 }
133}
134
135pub fn apt_upgrade() -> ActionSpec {
140 ActionSpec {
141 action_name: "AptUpgrade",
142 mechanism: command_mechanism(
143 "sudo",
144 [
145 "env",
146 DEBIAN_FRONTEND_VALUE,
147 NEEDRESTART_MODE_VALUE,
148 APT_GET,
149 "dist-upgrade",
150 "-y",
151 ],
152 ),
153 risk_level: RiskLevel::High,
154 reboot_required: false,
155 rollback_available: false,
156 }
157}
158
159pub fn apt_install(package: &str) -> ActionSpec {
164 ActionSpec {
165 action_name: "AptInstall",
166 mechanism: command_mechanism(
167 "sudo",
168 [
169 "env",
170 DEBIAN_FRONTEND_VALUE,
171 NEEDRESTART_MODE_VALUE,
172 APT_GET,
173 "install",
174 "-y",
175 package,
176 ],
177 ),
178 risk_level: RiskLevel::Medium,
179 reboot_required: false,
180 rollback_available: false,
181 }
182}
183
184pub fn apt_remove(package: &str) -> ActionSpec {
189 ActionSpec {
190 action_name: "AptRemove",
191 mechanism: command_mechanism(
192 "sudo",
193 [
194 "env",
195 DEBIAN_FRONTEND_VALUE,
196 NEEDRESTART_MODE_VALUE,
197 APT_GET,
198 "remove",
199 "-y",
200 package,
201 ],
202 ),
203 risk_level: RiskLevel::Medium,
204 reboot_required: false,
205 rollback_available: false,
206 }
207}
208
209pub fn apt_purge(package: &str) -> ActionSpec {
214 ActionSpec {
215 action_name: "AptPurge",
216 mechanism: command_mechanism(
217 "sudo",
218 [
219 "env",
220 DEBIAN_FRONTEND_VALUE,
221 NEEDRESTART_MODE_VALUE,
222 APT_GET,
223 "purge",
224 "-y",
225 package,
226 ],
227 ),
228 risk_level: RiskLevel::Medium,
229 reboot_required: false,
230 rollback_available: false,
231 }
232}
233
234pub fn apt_autoremove() -> ActionSpec {
243 ActionSpec {
244 action_name: "AptAutoremove",
245 mechanism: command_mechanism(
246 "sudo",
247 [
248 "env",
249 DEBIAN_FRONTEND_VALUE,
250 NEEDRESTART_MODE_VALUE,
251 APT_GET,
252 "autoremove",
253 "-y",
254 ],
255 ),
256 risk_level: RiskLevel::Medium,
257 reboot_required: false,
258 rollback_available: false,
259 }
260}
261
262pub fn apt_hold(package: &str) -> ActionSpec {
267 ActionSpec {
268 action_name: "AptHold",
269 mechanism: command_mechanism("sudo", ["apt-mark", "hold", package]),
270 risk_level: RiskLevel::Medium,
271 reboot_required: false,
272 rollback_available: false,
273 }
274}
275
276pub fn apt_unhold(package: &str) -> ActionSpec {
280 ActionSpec {
281 action_name: "AptUnhold",
282 mechanism: command_mechanism("sudo", ["apt-mark", "unhold", package]),
283 risk_level: RiskLevel::Medium,
284 reboot_required: false,
285 rollback_available: false,
286 }
287}
288
289pub fn apt_search(term: &str) -> ActionSpec {
293 ActionSpec {
294 action_name: "AptSearch",
295 mechanism: command_mechanism("apt-cache", ["search", term]),
296 risk_level: RiskLevel::Low,
297 reboot_required: false,
298 rollback_available: false,
299 }
300}
301
302pub fn apt_list_installed() -> ActionSpec {
306 ActionSpec {
307 action_name: "AptListInstalled",
308 mechanism: command_mechanism("dpkg", ["-l"]),
309 risk_level: RiskLevel::Low,
310 reboot_required: false,
311 rollback_available: false,
312 }
313}
314
315pub fn apt_show(package: &str) -> ActionSpec {
319 ActionSpec {
320 action_name: "AptShow",
321 mechanism: command_mechanism("apt-cache", ["show", package]),
322 risk_level: RiskLevel::Low,
323 reboot_required: false,
324 rollback_available: false,
325 }
326}
327
328pub fn apt_list_upgradable() -> ActionSpec {
336 ActionSpec {
337 action_name: "AptListUpgradable",
338 mechanism: command_mechanism("bash", ["-c", "apt list --upgradable 2>/dev/null"]),
339 risk_level: RiskLevel::Low,
340 reboot_required: false,
341 rollback_available: false,
342 }
343}
344
345pub fn apt_history_list() -> ActionSpec {
351 ActionSpec {
352 action_name: "AptHistoryList",
353 mechanism: command_mechanism(
354 "bash",
355 [
356 "-c",
357 "grep -A 4 '^Start-Date' /var/log/apt/history.log | tail -n 80",
358 ],
359 ),
360 risk_level: RiskLevel::Low,
361 reboot_required: false,
362 rollback_available: false,
363 }
364}
365
366#[cfg(test)]
371mod tests {
372 use super::*;
373 use crate::actions::ActionMechanism;
374
375 fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<&str>) {
376 match &spec.mechanism {
377 ActionMechanism::Command { program, args } => {
378 (*program, args.iter().map(String::as_str).collect())
379 }
380 _ => panic!("expected Command mechanism"),
381 }
382 }
383
384 #[test]
387 fn apt_update_action_name() {
388 assert_eq!(apt_update().action_name, "AptUpdate");
389 }
390
391 #[test]
392 fn apt_update_argv_correct() {
393 let spec = apt_update();
394 let (prog, args) = extract_args(&spec);
395 assert_eq!(prog, "sudo");
396 assert!(
397 args.contains(&APT_GET),
398 "argv must carry the absolute apt-get path: {args:?}"
399 );
400 assert!(args.contains(&"update"));
401 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
402 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
403 }
404
405 #[test]
406 fn apt_update_risk_is_low() {
407 assert_eq!(apt_update().risk_level, RiskLevel::Low);
408 }
409
410 #[test]
411 fn apt_update_no_reboot_no_rollback() {
412 let spec = apt_update();
413 assert!(!spec.reboot_required);
414 assert!(!spec.rollback_available);
415 }
416
417 #[test]
420 fn apt_upgrade_action_name() {
421 assert_eq!(apt_upgrade().action_name, "AptUpgrade");
422 }
423
424 #[test]
425 fn apt_upgrade_argv_correct() {
426 let spec = apt_upgrade();
427 let (prog, args) = extract_args(&spec);
428 assert_eq!(prog, "sudo");
429 assert!(
430 args.contains(&APT_GET),
431 "argv must carry the absolute apt-get path: {args:?}"
432 );
433 assert!(args.contains(&"dist-upgrade"));
434 assert!(args.contains(&"-y"));
435 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
436 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
437 }
438
439 #[test]
440 fn apt_upgrade_risk_is_high() {
441 assert_eq!(apt_upgrade().risk_level, RiskLevel::High);
442 }
443
444 #[test]
447 fn apt_install_action_name() {
448 assert_eq!(apt_install("vim").action_name, "AptInstall");
449 }
450
451 #[test]
452 fn apt_install_includes_package_name() {
453 let spec = apt_install("vim");
454 let (_, args) = extract_args(&spec);
455 assert!(args.contains(&"vim"));
456 assert!(args.contains(&"install"));
457 assert!(args.contains(&"-y"));
458 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
459 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
460 }
461
462 #[test]
463 fn apt_install_risk_is_medium() {
464 assert_eq!(apt_install("vim").risk_level, RiskLevel::Medium);
465 }
466
467 #[test]
470 fn apt_remove_action_name() {
471 assert_eq!(apt_remove("vim").action_name, "AptRemove");
472 }
473
474 #[test]
475 fn apt_remove_argv_contains_remove_not_purge() {
476 let spec = apt_remove("vim");
477 let (_, args) = extract_args(&spec);
478 assert!(args.contains(&"remove"));
479 assert!(!args.contains(&"purge"));
481 assert!(args.contains(&"vim"));
482 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
483 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
484 }
485
486 #[test]
487 fn apt_remove_risk_is_medium() {
488 assert_eq!(apt_remove("vim").risk_level, RiskLevel::Medium);
489 }
490
491 #[test]
494 fn apt_purge_action_name() {
495 assert_eq!(apt_purge("vim").action_name, "AptPurge");
496 }
497
498 #[test]
499 fn apt_purge_argv_uses_purge_subcommand() {
500 let spec = apt_purge("vim");
501 let (_, args) = extract_args(&spec);
502 assert!(args.contains(&"purge"));
503 assert!(args.contains(&"vim"));
504 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
505 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
506 }
507
508 #[test]
509 fn apt_purge_risk_is_medium() {
510 assert_eq!(apt_purge("vim").risk_level, RiskLevel::Medium);
511 }
512
513 #[test]
516 fn apt_autoremove_action_name() {
517 assert_eq!(apt_autoremove().action_name, "AptAutoremove");
518 }
519
520 #[test]
521 fn apt_autoremove_argv_correct() {
522 let spec = apt_autoremove();
523 let (prog, args) = extract_args(&spec);
524 assert_eq!(prog, "sudo");
525 assert!(args.contains(&"autoremove"));
526 assert!(args.contains(&"-y"));
527 assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
528 assert!(args.contains(&NEEDRESTART_MODE_VALUE));
529 }
530
531 #[test]
532 fn apt_autoremove_risk_is_medium() {
533 assert_eq!(apt_autoremove().risk_level, RiskLevel::Medium);
534 }
535
536 #[test]
539 fn apt_hold_action_name() {
540 assert_eq!(apt_hold("nginx").action_name, "AptHold");
541 }
542
543 #[test]
544 fn apt_hold_uses_apt_mark_hold() {
545 let spec = apt_hold("nginx");
546 let (prog, args) = extract_args(&spec);
547 assert_eq!(prog, "sudo");
548 assert!(args.contains(&"apt-mark"));
549 assert!(args.contains(&"hold"));
550 assert!(args.contains(&"nginx"));
551 }
552
553 #[test]
554 fn apt_hold_risk_is_medium() {
555 assert_eq!(apt_hold("nginx").risk_level, RiskLevel::Medium);
556 }
557
558 #[test]
561 fn apt_unhold_action_name() {
562 assert_eq!(apt_unhold("nginx").action_name, "AptUnhold");
563 }
564
565 #[test]
566 fn apt_unhold_uses_apt_mark_unhold() {
567 let spec = apt_unhold("nginx");
568 let (_, args) = extract_args(&spec);
569 assert!(args.contains(&"apt-mark"));
570 assert!(args.contains(&"unhold"));
571 assert!(args.contains(&"nginx"));
572 }
573
574 #[test]
575 fn apt_unhold_risk_is_medium() {
576 assert_eq!(apt_unhold("nginx").risk_level, RiskLevel::Medium);
577 }
578
579 #[test]
582 fn apt_search_action_name() {
583 assert_eq!(apt_search("docker").action_name, "AptSearch");
584 }
585
586 #[test]
587 fn apt_search_uses_apt_cache_search() {
588 let spec = apt_search("docker");
589 let (prog, args) = extract_args(&spec);
590 assert_eq!(prog, "apt-cache");
591 assert!(args.contains(&"search"));
592 assert!(args.contains(&"docker"));
593 }
594
595 #[test]
596 fn apt_search_risk_is_low() {
597 assert_eq!(apt_search("docker").risk_level, RiskLevel::Low);
598 }
599
600 #[test]
601 fn apt_search_no_sudo() {
602 let spec = apt_search("docker");
604 let (prog, _) = extract_args(&spec);
605 assert_ne!(prog, "sudo");
606 }
607
608 #[test]
611 fn apt_list_installed_action_name() {
612 assert_eq!(apt_list_installed().action_name, "AptListInstalled");
613 }
614
615 #[test]
616 fn apt_list_installed_uses_dpkg() {
617 let spec = apt_list_installed();
618 let (prog, args) = extract_args(&spec);
619 assert_eq!(prog, "dpkg");
620 assert!(args.contains(&"-l"));
621 }
622
623 #[test]
624 fn apt_list_installed_risk_is_low() {
625 assert_eq!(apt_list_installed().risk_level, RiskLevel::Low);
626 }
627
628 #[test]
631 fn apt_show_action_name() {
632 assert_eq!(apt_show("openssh-server").action_name, "AptShow");
633 }
634
635 #[test]
636 fn apt_show_uses_apt_cache_show() {
637 let spec = apt_show("openssh-server");
638 let (prog, args) = extract_args(&spec);
639 assert_eq!(prog, "apt-cache");
640 assert!(args.contains(&"show"));
641 assert!(args.contains(&"openssh-server"));
642 }
643
644 #[test]
645 fn apt_show_risk_is_low() {
646 assert_eq!(apt_show("openssh-server").risk_level, RiskLevel::Low);
647 }
648
649 #[test]
652 fn apt_list_upgradable_action_name() {
653 assert_eq!(apt_list_upgradable().action_name, "AptListUpgradable");
654 }
655
656 #[test]
657 fn apt_list_upgradable_uses_bash_and_apt_list() {
658 let spec = apt_list_upgradable();
659 match &spec.mechanism {
660 ActionMechanism::Command { program, args } => {
661 assert_eq!(*program, "bash");
662 let joined = args.join(" ");
663 assert!(joined.contains("apt list --upgradable"), "argv: {joined}");
664 }
665 _ => panic!("expected Command mechanism"),
666 }
667 }
668
669 #[test]
670 fn apt_list_upgradable_risk_is_low() {
671 assert_eq!(apt_list_upgradable().risk_level, RiskLevel::Low);
672 }
673
674 #[test]
677 fn apt_history_list_action_name() {
678 assert_eq!(apt_history_list().action_name, "AptHistoryList");
679 }
680
681 #[test]
682 fn apt_history_list_uses_bash_and_grep() {
683 let spec = apt_history_list();
684 match &spec.mechanism {
685 ActionMechanism::Command { program, args } => {
686 assert_eq!(*program, "bash");
687 let joined = args.join(" ");
688 assert!(
689 joined.contains("/var/log/apt/history.log"),
690 "argv: {joined}"
691 );
692 }
693 _ => panic!("expected Command mechanism"),
694 }
695 }
696
697 #[test]
698 fn apt_history_list_risk_is_low() {
699 assert_eq!(apt_history_list().risk_level, RiskLevel::Low);
700 }
701
702 #[test]
705 fn specs_covers_all_action_names() {
706 let expected_names = [
707 "AptUpdate",
708 "AptUpgrade",
709 "AptInstall",
710 "AptRemove",
711 "AptPurge",
712 "AptAutoremove",
713 "AptHold",
714 "AptUnhold",
715 "AptSearch",
716 "AptListInstalled",
717 "AptShow",
718 "AptListUpgradable",
719 "AptHistoryList",
720 ];
721 let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
722 for name in &expected_names {
723 assert!(
724 spec_names.contains(name),
725 "specs() is missing action {name}"
726 );
727 }
728 }
729
730 #[test]
733 fn mutating_apt_commands_carry_env_vars() {
734 let mutating = [
735 apt_upgrade(),
736 apt_install("vim"),
737 apt_remove("vim"),
738 apt_purge("vim"),
739 apt_autoremove(),
740 ];
741 for spec in &mutating {
742 let (_, args) = extract_args(spec);
743 assert!(
744 args.contains(&DEBIAN_FRONTEND_VALUE),
745 "{} missing DEBIAN_FRONTEND",
746 spec.action_name
747 );
748 assert!(
749 args.contains(&NEEDRESTART_MODE_VALUE),
750 "{} missing NEEDRESTART_MODE",
751 spec.action_name
752 );
753 }
754 }
755}