check_macos_updates/
lib.rs

1use nagios_range::Error as RangeError;
2use nagios_range::NagiosRange as ThresholdRange;
3use serde::Deserialize;
4use std::fmt;
5use std::process::{self, Output};
6
7pub const PLIST_FILE: &str = "/Library/Preferences/com.apple.SoftwareUpdate.plist";
8
9#[derive(Clone, Debug, PartialEq)]
10pub struct Thresholds {
11    pub warning: Option<ThresholdRange>,
12    pub critical: Option<ThresholdRange>,
13}
14
15#[non_exhaustive]
16#[derive(Debug, PartialEq)]
17pub enum UnkownVariant {
18    ClapError(String),
19    NotMacOS,
20    NoThresholds,
21    RangeParseError(String, RangeError),
22    UnableToDetermineUpdates,
23    UnableToParsePlist,
24}
25
26#[derive(Debug, PartialEq)]
27pub enum Status {
28    Ok(usize),
29    Warning(usize),
30    Critical(usize),
31    Unknown(UnkownVariant),
32}
33
34impl fmt::Display for Status {
35    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
36        match self {
37            Status::Ok(n) => write!(f, "OK - {} updates available|'Available Updates'={}", n, n),
38            Status::Warning(n) => write!(
39                f,
40                "WARNING - Updates available: {}|'Available Updates'={}",
41                n, n
42            ),
43            Status::Critical(n) => write!(
44                f,
45                "CRITICAL - Updates available: {}|'Available Updates'={}",
46                n, n
47            ),
48            Status::Unknown(UnkownVariant::ClapError(s)) => {
49                let trimmed = s.trim_end();
50                let without_leading_error = trimmed.trim_start_matches("error: ");
51                write!(
52                    f,
53                    "UNKNOWN - Command line parsing produced an error: {}",
54                    without_leading_error,
55                )
56            }
57            Status::Unknown(UnkownVariant::NotMacOS) => {
58                write!(f, "UNKNOWN - Not running on macOS")
59            }
60            Status::Unknown(UnkownVariant::NoThresholds) => {
61                write!(f, "UNKNOWN - No thresholds provided")
62            }
63            Status::Unknown(UnkownVariant::RangeParseError(s, e)) => {
64                write!(
65                    f,
66                    "UNKNOWN - Unable to parse range '{}' with error: {}",
67                    s, e
68                )
69            }
70            Status::Unknown(UnkownVariant::UnableToDetermineUpdates) => {
71                write!(f, "UNKNOWN - Unable to determine available updates")
72            }
73            Status::Unknown(UnkownVariant::UnableToParsePlist) => {
74                write!(f, "UNKNOWN - Unable to parse plist file")
75            }
76        }
77    }
78}
79
80impl Status {
81    pub fn to_int(&self) -> i32 {
82        match self {
83            Status::Ok(_) => 0,
84            Status::Warning(_) => 1,
85            Status::Critical(_) => 2,
86            Status::Unknown(_) => 3,
87        }
88    }
89}
90
91// See tests/plist_examples.rs for examples of the plist file.
92#[derive(Debug, Deserialize)]
93#[serde(rename_all = "PascalCase")]
94pub struct SoftwareUpdate {
95    #[serde(default)]
96    pub automatic_check_enabled: bool,
97    // #[serde(default)]
98    // pub automatic_download: bool,
99    // pub last_successful_date: String,
100    // pub last_attempt_system_version: String,
101    pub last_updates_available: u8,
102    // pub last_recommended_updates_available: u8,
103    // pub last_attempt_build_version: String,
104    // pub recommended_updates: Vec<String>,
105    // pub last_full_successful_date: String,
106    // pub primary_languages: Vec<String>,
107    // pub last_session_successful: bool,
108    // pub last_background_successful_date: String,
109    // pub last_result_code: u8,
110}
111
112pub fn softwareupdate_output() -> Result<Output, std::io::Error> {
113    process::Command::new("softwareupdate").arg("-l").output()
114}
115
116fn evaluate_thresholds(n: usize, thresholds: &Thresholds) -> Status {
117    if let Some(c) = thresholds.critical {
118        if c.check(n as f64) {
119            return Status::Critical(n);
120        }
121    }
122    if let Some(w) = thresholds.warning {
123        if w.check(n as f64) {
124            return Status::Warning(n);
125        }
126    }
127    Status::Ok(n)
128}
129
130pub fn check_softwareupdate_output(
131    output: &Result<Output, std::io::Error>,
132    thresholds: &Thresholds,
133) -> Status {
134    match output {
135        Ok(output) => {
136            let output_stderr = String::from_utf8_lossy(&output.stderr);
137            let output_stdout = String::from_utf8_lossy(&output.stdout);
138
139            let n: usize = if output_stderr.contains("No new software available.") {
140                0
141            } else {
142                output_stdout
143                    .lines()
144                    .filter(|l| l.contains("* Label:"))
145                    .count()
146            };
147
148            evaluate_thresholds(n, thresholds)
149        }
150        Err(_) => Status::Unknown(UnkownVariant::UnableToDetermineUpdates),
151    }
152}
153
154pub fn determine_updates(update: &SoftwareUpdate, thresholds: &Thresholds) -> Status {
155    let n = update.last_updates_available as usize;
156    if !update.automatic_check_enabled && n == 0 {
157        check_softwareupdate_output(&softwareupdate_output(), thresholds)
158    } else {
159        evaluate_thresholds(n, thresholds)
160    }
161}