check_macos_updates/
lib.rs1use 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#[derive(Debug, Deserialize)]
93#[serde(rename_all = "PascalCase")]
94pub struct SoftwareUpdate {
95 #[serde(default)]
96 pub automatic_check_enabled: bool,
97 pub last_updates_available: u8,
102 }
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}