1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use nagios_range::Error as RangeError;
use nagios_range::NagiosRange as ThresholdRange;
use serde::Deserialize;
use std::fmt;
use std::process::{self, Output};

pub const PLIST_FILE: &str = "/Library/Preferences/com.apple.SoftwareUpdate.plist";

#[derive(Clone, Debug, PartialEq)]
pub struct Thresholds {
    pub warning: Option<ThresholdRange>,
    pub critical: Option<ThresholdRange>,
}

#[non_exhaustive]
#[derive(Debug, PartialEq)]
pub enum UnkownVariant {
    NotMacOS,
    NoThresholds,
    RangeParseError(String, RangeError),
    UnableToDetermineUpdates,
    UnableToParsePlist,
}

#[derive(Debug, PartialEq)]
pub enum Status {
    Ok(usize),
    Warning(usize),
    Critical(usize),
    Unknown(UnkownVariant),
}

impl fmt::Display for Status {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Status::Ok(n) => write!(f, "OK - {} updates available|'Available Updates'={}", n, n),
            Status::Warning(n) => write!(
                f,
                "WARNING - Updates available: {}|'Available Updates'={}",
                n, n
            ),
            Status::Critical(n) => write!(
                f,
                "CRITICAL - Updates available: {}|'Available Updates'={}",
                n, n
            ),
            Status::Unknown(UnkownVariant::NotMacOS) => {
                write!(f, "UNKNOWN - Not running on macOS")
            }
            Status::Unknown(UnkownVariant::NoThresholds) => {
                write!(f, "UNKNOWN - No thresholds provided")
            }
            Status::Unknown(UnkownVariant::RangeParseError(s, e)) => {
                write!(
                    f,
                    "UNKNOWN - Unable to parse range '{}' with error: {}",
                    s, e
                )
            }
            Status::Unknown(UnkownVariant::UnableToDetermineUpdates) => {
                write!(f, "UNKNOWN - Unable to determine available updates")
            }
            Status::Unknown(UnkownVariant::UnableToParsePlist) => {
                write!(f, "UNKNOWN - Unable to parse plist file")
            }
        }
    }
}

impl Status {
    pub fn to_int(&self) -> i32 {
        match self {
            Status::Ok(_) => 0,
            Status::Warning(_) => 1,
            Status::Critical(_) => 2,
            Status::Unknown(_) => 3,
        }
    }
}

// See tests/plist_examples.rs for examples of the plist file.
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct SoftwareUpdate {
    #[serde(default)]
    pub automatic_check_enabled: bool,
    // #[serde(default)]
    // pub automatic_download: bool,
    // pub last_successful_date: String,
    // pub last_attempt_system_version: String,
    pub last_updates_available: u8,
    // pub last_recommended_updates_available: u8,
    // pub last_attempt_build_version: String,
    // pub recommended_updates: Vec<String>,
    // pub last_full_successful_date: String,
    // pub primary_languages: Vec<String>,
    // pub last_session_successful: bool,
    // pub last_background_successful_date: String,
    // pub last_result_code: u8,
}

pub fn softwareupdate_output() -> Result<Output, std::io::Error> {
    process::Command::new("softwareupdate").arg("-l").output()
}

fn evaluate_thresholds(n: usize, thresholds: &Thresholds) -> Status {
    if let Some(c) = thresholds.critical {
        if c.check(n as f64) {
            return Status::Critical(n);
        }
    }
    if let Some(w) = thresholds.warning {
        if w.check(n as f64) {
            return Status::Warning(n);
        }
    }
    Status::Ok(n)
}

pub fn check_softwareupdate_output(
    output: &Result<Output, std::io::Error>,
    thresholds: &Thresholds,
) -> Status {
    match output {
        Ok(output) => {
            let output_stderr = String::from_utf8_lossy(&output.stderr);
            let output_stdout = String::from_utf8_lossy(&output.stdout);

            let n: usize = if output_stderr.contains("No new software available.") {
                0
            } else {
                output_stdout
                    .lines()
                    .filter(|l| l.contains("* Label:"))
                    .count()
            };

            evaluate_thresholds(n, thresholds)
        }
        Err(_) => Status::Unknown(UnkownVariant::UnableToDetermineUpdates),
    }
}

pub fn determine_updates(update: &SoftwareUpdate, thresholds: &Thresholds) -> Status {
    let n = update.last_updates_available as usize;
    if !update.automatic_check_enabled && n == 0 {
        check_softwareupdate_output(&softwareupdate_output(), thresholds)
    } else {
        evaluate_thresholds(n, thresholds)
    }
}