use super::{command_mechanism, ActionSpec};
use sysknife_types::RiskLevel;
const APT_GET: &str = "/usr/bin/apt-get";
const DEBIAN_FRONTEND_VALUE: &str = "DEBIAN_FRONTEND=noninteractive";
const NEEDRESTART_MODE_VALUE: &str = "NEEDRESTART_MODE=a";
pub fn specs() -> Vec<ActionSpec> {
vec![
apt_update(),
apt_upgrade(),
apt_install("curl"),
apt_remove("curl"),
apt_purge("curl"),
apt_autoremove(),
apt_hold("curl"),
apt_unhold("curl"),
apt_search("curl"),
apt_list_installed(),
apt_show("curl"),
apt_list_upgradable(),
apt_history_list(),
configure_unattended_upgrades(true),
]
}
const UNATTENDED_UPGRADES_HELPER: &str = "/usr/lib/sysknife/unattended-upgrades-edit";
pub fn configure_unattended_upgrades(enabled: bool) -> ActionSpec {
let flag = if enabled { "--enable" } else { "--disable" };
ActionSpec {
action_name: "ConfigureUnattendedUpgrades",
mechanism: command_mechanism("sudo", [UNATTENDED_UPGRADES_HELPER, flag]),
risk_level: RiskLevel::High,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_update() -> ActionSpec {
ActionSpec {
action_name: "AptUpdate",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"update",
],
),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_upgrade() -> ActionSpec {
ActionSpec {
action_name: "AptUpgrade",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"dist-upgrade",
"-y",
],
),
risk_level: RiskLevel::High,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_install(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptInstall",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"install",
"-y",
package,
],
),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_remove(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptRemove",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"remove",
"-y",
package,
],
),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_purge(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptPurge",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"purge",
"-y",
package,
],
),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_autoremove() -> ActionSpec {
ActionSpec {
action_name: "AptAutoremove",
mechanism: command_mechanism(
"sudo",
[
"env",
DEBIAN_FRONTEND_VALUE,
NEEDRESTART_MODE_VALUE,
APT_GET,
"autoremove",
"-y",
],
),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_hold(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptHold",
mechanism: command_mechanism("sudo", ["apt-mark", "hold", package]),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_unhold(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptUnhold",
mechanism: command_mechanism("sudo", ["apt-mark", "unhold", package]),
risk_level: RiskLevel::Medium,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_search(term: &str) -> ActionSpec {
ActionSpec {
action_name: "AptSearch",
mechanism: command_mechanism("apt-cache", ["search", term]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_list_installed() -> ActionSpec {
ActionSpec {
action_name: "AptListInstalled",
mechanism: command_mechanism("dpkg", ["-l"]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_show(package: &str) -> ActionSpec {
ActionSpec {
action_name: "AptShow",
mechanism: command_mechanism("apt-cache", ["show", package]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_list_upgradable() -> ActionSpec {
ActionSpec {
action_name: "AptListUpgradable",
mechanism: command_mechanism("bash", ["-c", "apt list --upgradable 2>/dev/null"]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
pub fn apt_history_list() -> ActionSpec {
ActionSpec {
action_name: "AptHistoryList",
mechanism: command_mechanism(
"bash",
[
"-c",
"grep -A 4 '^Start-Date' /var/log/apt/history.log | tail -n 80",
],
),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::ActionMechanism;
fn extract_args(spec: &ActionSpec) -> (&'static str, Vec<&str>) {
match &spec.mechanism {
ActionMechanism::Command { program, args } => {
(*program, args.iter().map(String::as_str).collect())
}
_ => panic!("expected Command mechanism"),
}
}
#[test]
fn apt_update_action_name() {
assert_eq!(apt_update().action_name, "AptUpdate");
}
#[test]
fn apt_update_argv_correct() {
let spec = apt_update();
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "sudo");
assert!(
args.contains(&APT_GET),
"argv must carry the absolute apt-get path: {args:?}"
);
assert!(args.contains(&"update"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_update_risk_is_low() {
assert_eq!(apt_update().risk_level, RiskLevel::Low);
}
#[test]
fn apt_update_no_reboot_no_rollback() {
let spec = apt_update();
assert!(!spec.reboot_required);
assert!(!spec.rollback_available);
}
#[test]
fn apt_upgrade_action_name() {
assert_eq!(apt_upgrade().action_name, "AptUpgrade");
}
#[test]
fn apt_upgrade_argv_correct() {
let spec = apt_upgrade();
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "sudo");
assert!(
args.contains(&APT_GET),
"argv must carry the absolute apt-get path: {args:?}"
);
assert!(args.contains(&"dist-upgrade"));
assert!(args.contains(&"-y"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_upgrade_risk_is_high() {
assert_eq!(apt_upgrade().risk_level, RiskLevel::High);
}
#[test]
fn apt_install_action_name() {
assert_eq!(apt_install("vim").action_name, "AptInstall");
}
#[test]
fn apt_install_includes_package_name() {
let spec = apt_install("vim");
let (_, args) = extract_args(&spec);
assert!(args.contains(&"vim"));
assert!(args.contains(&"install"));
assert!(args.contains(&"-y"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_install_risk_is_medium() {
assert_eq!(apt_install("vim").risk_level, RiskLevel::Medium);
}
#[test]
fn apt_remove_action_name() {
assert_eq!(apt_remove("vim").action_name, "AptRemove");
}
#[test]
fn apt_remove_argv_contains_remove_not_purge() {
let spec = apt_remove("vim");
let (_, args) = extract_args(&spec);
assert!(args.contains(&"remove"));
assert!(!args.contains(&"purge"));
assert!(args.contains(&"vim"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_remove_risk_is_medium() {
assert_eq!(apt_remove("vim").risk_level, RiskLevel::Medium);
}
#[test]
fn apt_purge_action_name() {
assert_eq!(apt_purge("vim").action_name, "AptPurge");
}
#[test]
fn apt_purge_argv_uses_purge_subcommand() {
let spec = apt_purge("vim");
let (_, args) = extract_args(&spec);
assert!(args.contains(&"purge"));
assert!(args.contains(&"vim"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_purge_risk_is_medium() {
assert_eq!(apt_purge("vim").risk_level, RiskLevel::Medium);
}
#[test]
fn apt_autoremove_action_name() {
assert_eq!(apt_autoremove().action_name, "AptAutoremove");
}
#[test]
fn apt_autoremove_argv_correct() {
let spec = apt_autoremove();
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "sudo");
assert!(args.contains(&"autoremove"));
assert!(args.contains(&"-y"));
assert!(args.contains(&DEBIAN_FRONTEND_VALUE));
assert!(args.contains(&NEEDRESTART_MODE_VALUE));
}
#[test]
fn apt_autoremove_risk_is_medium() {
assert_eq!(apt_autoremove().risk_level, RiskLevel::Medium);
}
#[test]
fn apt_hold_action_name() {
assert_eq!(apt_hold("nginx").action_name, "AptHold");
}
#[test]
fn apt_hold_uses_apt_mark_hold() {
let spec = apt_hold("nginx");
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "sudo");
assert!(args.contains(&"apt-mark"));
assert!(args.contains(&"hold"));
assert!(args.contains(&"nginx"));
}
#[test]
fn apt_hold_risk_is_medium() {
assert_eq!(apt_hold("nginx").risk_level, RiskLevel::Medium);
}
#[test]
fn apt_unhold_action_name() {
assert_eq!(apt_unhold("nginx").action_name, "AptUnhold");
}
#[test]
fn apt_unhold_uses_apt_mark_unhold() {
let spec = apt_unhold("nginx");
let (_, args) = extract_args(&spec);
assert!(args.contains(&"apt-mark"));
assert!(args.contains(&"unhold"));
assert!(args.contains(&"nginx"));
}
#[test]
fn apt_unhold_risk_is_medium() {
assert_eq!(apt_unhold("nginx").risk_level, RiskLevel::Medium);
}
#[test]
fn apt_search_action_name() {
assert_eq!(apt_search("docker").action_name, "AptSearch");
}
#[test]
fn apt_search_uses_apt_cache_search() {
let spec = apt_search("docker");
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "apt-cache");
assert!(args.contains(&"search"));
assert!(args.contains(&"docker"));
}
#[test]
fn apt_search_risk_is_low() {
assert_eq!(apt_search("docker").risk_level, RiskLevel::Low);
}
#[test]
fn apt_search_no_sudo() {
let spec = apt_search("docker");
let (prog, _) = extract_args(&spec);
assert_ne!(prog, "sudo");
}
#[test]
fn apt_list_installed_action_name() {
assert_eq!(apt_list_installed().action_name, "AptListInstalled");
}
#[test]
fn apt_list_installed_uses_dpkg() {
let spec = apt_list_installed();
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "dpkg");
assert!(args.contains(&"-l"));
}
#[test]
fn apt_list_installed_risk_is_low() {
assert_eq!(apt_list_installed().risk_level, RiskLevel::Low);
}
#[test]
fn apt_show_action_name() {
assert_eq!(apt_show("openssh-server").action_name, "AptShow");
}
#[test]
fn apt_show_uses_apt_cache_show() {
let spec = apt_show("openssh-server");
let (prog, args) = extract_args(&spec);
assert_eq!(prog, "apt-cache");
assert!(args.contains(&"show"));
assert!(args.contains(&"openssh-server"));
}
#[test]
fn apt_show_risk_is_low() {
assert_eq!(apt_show("openssh-server").risk_level, RiskLevel::Low);
}
#[test]
fn apt_list_upgradable_action_name() {
assert_eq!(apt_list_upgradable().action_name, "AptListUpgradable");
}
#[test]
fn apt_list_upgradable_uses_bash_and_apt_list() {
let spec = apt_list_upgradable();
match &spec.mechanism {
ActionMechanism::Command { program, args } => {
assert_eq!(*program, "bash");
let joined = args.join(" ");
assert!(joined.contains("apt list --upgradable"), "argv: {joined}");
}
_ => panic!("expected Command mechanism"),
}
}
#[test]
fn apt_list_upgradable_risk_is_low() {
assert_eq!(apt_list_upgradable().risk_level, RiskLevel::Low);
}
#[test]
fn apt_history_list_action_name() {
assert_eq!(apt_history_list().action_name, "AptHistoryList");
}
#[test]
fn apt_history_list_uses_bash_and_grep() {
let spec = apt_history_list();
match &spec.mechanism {
ActionMechanism::Command { program, args } => {
assert_eq!(*program, "bash");
let joined = args.join(" ");
assert!(
joined.contains("/var/log/apt/history.log"),
"argv: {joined}"
);
}
_ => panic!("expected Command mechanism"),
}
}
#[test]
fn apt_history_list_risk_is_low() {
assert_eq!(apt_history_list().risk_level, RiskLevel::Low);
}
#[test]
fn specs_covers_all_action_names() {
let expected_names = [
"AptUpdate",
"AptUpgrade",
"AptInstall",
"AptRemove",
"AptPurge",
"AptAutoremove",
"AptHold",
"AptUnhold",
"AptSearch",
"AptListInstalled",
"AptShow",
"AptListUpgradable",
"AptHistoryList",
];
let spec_names: Vec<&str> = specs().iter().map(|s| s.action_name).collect();
for name in &expected_names {
assert!(
spec_names.contains(name),
"specs() is missing action {name}"
);
}
}
#[test]
fn mutating_apt_commands_carry_env_vars() {
let mutating = [
apt_upgrade(),
apt_install("vim"),
apt_remove("vim"),
apt_purge("vim"),
apt_autoremove(),
];
for spec in &mutating {
let (_, args) = extract_args(spec);
assert!(
args.contains(&DEBIAN_FRONTEND_VALUE),
"{} missing DEBIAN_FRONTEND",
spec.action_name
);
assert!(
args.contains(&NEEDRESTART_MODE_VALUE),
"{} missing NEEDRESTART_MODE",
spec.action_name
);
}
}
}