├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── img ├── .gitignore ├── jb-img.png └── screengrab.jpg ├── local.lcars.macOSSecurityUpdates.plist └── macsu.zsh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .CS_Store 3 | TODO 4 | OUT -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018–20 Joss Brown (pseud.) -- German laws apply -- Place of jurisdiction: Berlin, Germany 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![macsu-platform-macos](https://img.shields.io/badge/platform-macOS-lightgrey.svg) 2 | ![macsu-code-shell](https://img.shields.io/badge/code-shell-yellow.svg) 3 | [![macsu-license](http://img.shields.io/badge/license-MIT+-blue.svg)](https://github.com/JayBrown/macOS-Security-Updates/blob/master/LICENSE) 4 | 5 | # macOS Security Updates (macSU) 6 | 7 | **macOS Security Updates (macSU) is a LaunchAgent and shell script for macOS 10.15 (Catalina). It will run a scan every four hours and notify the user if any of the following macOS Security components has been been updated:** 8 | * **Gatekeeper** 9 | * **Gatekeeper E** 10 | * **Incompatible Kernel Extensions (KEXT Exclusions)** 11 | * **Malware Removal Tool (MRT)** 12 | * **TCC** 13 | * **XProtect** 14 | 15 | **Plus:** 16 | * **App Exceptions** 17 | * **Compatibility Notification Data** 18 | * **Core LSKD (kdrl)** 19 | * **Core Suggestions** 20 | * **Incompatible Apps** 21 | 22 | **Plus:** 23 | * **System** 24 | * **System build** 25 | * **EFI (Boot ROM)** 26 | * **iBridge** 27 | * **rootless.conf** 28 | 29 | **macSU now also checks against a remote database (hosted on GitHub) containing the current version numbers of the more important macOS security components. They are the first six in the list above. If any of them is outdated, the user will be notified. macSU will not notify the user when the system itself (which mostly includes EFI and iBridge) is out-of-date, to account for users who do not wish to update to a new system (immediately).** 30 | 31 | ![screengrab](https://github.com/JayBrown/macOS-Security-Updates/blob/master/img/screengrab.jpg) 32 | 33 | ## Installation 34 | * clone repo 35 | * `chmod +x macsu.zsh && ln -s macsu.zsh /usr/local/bin/macsu.zsh` 36 | * `cp local.lcars.macOSSecurityUpdates.plist $HOME/Library/LaunchAgents/local.lcars.macOSSecurityUpdates.plist` 37 | * `launchctl load $HOME/Library/LaunchAgents/local.lcars.macOSSecurityUpdates.plist` 38 | * optional: install **[terminal-notifier](https://github.com/julienXX/terminal-notifier)** 39 | 40 | ### Testing 41 | **Execute `macsu.zsh` at least once**, e.g. by running the LaunchAgent with `launchctl start local.lcars.macOSSecurityUpdates`, or by calling the script directly: `./macsu.zsh` 42 | 43 | Then you can test the update notification functionality i.a. by entering the following command sequence: 44 | 45 | `plutil -replace CFBundleShortVersionString -integer 2098 "$HOME/.cache/macSU/XP-version.plist" && launchctl start local.lcars.macOSSecurityUpdates` 46 | 47 | ### Notes 48 | * The agent (and thereby the script) will run every 4 hours. 49 | * **macSU** is only compatible with macOS 10.15 (Catalina). 50 | 51 | ## Uninstall 52 | * `launchctl unload $HOME/Library/LaunchAgents/local.lcars.macOSSecurityUpdates.plist` 53 | * remove the cloned `macOS-Security-Updates` GitHub repository 54 | * `rm -f /usr/local/bin/macsu.zsh` 55 | * `rm -rf $HOME/.cache/macSU` 56 | * `rm -f $HOME/Library/Logs/local.lcars.macOSSecurityUpdates.log` 57 | * `rm -f /tmp/local.lcars.macOSSecurityUpdates.stdout` 58 | * `rm -f /tmp/local.lcars.macOSSecurityUpdates.stderr` 59 | 60 | ## Future 61 | * find a way to read the System Integrity Protection (SIP) version number on Catalina 62 | 63 | ## Thanks 64 | * **Howard Oakley** (@hoakleyelc) of **[EclecticLight](https://eclecticlight.co/)** for providing the [databases](https://github.com/hoakleyelc/updates) of current version numbers 65 | -------------------------------------------------------------------------------- /img/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .CS_Store 3 | TODO -------------------------------------------------------------------------------- /img/jb-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayBrown/macOS-Security-Updates/9b98595bd1cf4ba9c83ea8e080c8a1e5b1b42ddb/img/jb-img.png -------------------------------------------------------------------------------- /img/screengrab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayBrown/macOS-Security-Updates/9b98595bd1cf4ba9c83ea8e080c8a1e5b1b42ddb/img/screengrab.jpg -------------------------------------------------------------------------------- /local.lcars.macOSSecurityUpdates.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Label 6 | local.lcars.macOSSecurityUpdates 7 | LowPriorityBackgroundIO 8 | 9 | LowPriorityIO 10 | 11 | Nice 12 | 20 13 | ProcessType 14 | Background 15 | Program 16 | /usr/local/bin/macsu.zsh 17 | StandardErrorPath 18 | /tmp/local.lcars.macOSSecurityUpdates.stderr 19 | StandardOutPath 20 | /tmp/local.lcars.macOSSecurityUpdates.stdout 21 | StartInterval 22 | 14400 23 | 24 | 25 | -------------------------------------------------------------------------------- /macsu.zsh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | # shellcheck shell=bash 3 | 4 | # macOS Security Updates (macSU) 5 | # shell script: macsu.zsh / LaunchAgent: local.lcars.macOSSecurityUpdates 6 | # v2.1.4 7 | # Copyright (c) 2018–20 Joss Brown (pseud.) 8 | # license: MIT+ 9 | # info: https://github.com/JayBrown/macOS-Security-Updates 10 | # thanks to Howard Oakley: https://eclecticlight.co / https://github.com/hoakleyelc/updates 11 | 12 | export LANG=en_US.UTF-8 13 | export SYSTEM_VERSION_COMPAT=0 14 | 15 | macsuv="2.1.4" 16 | macsumv="2" 17 | scrname=$(basename "$0") 18 | process="macOS Security" 19 | account=$(id -u) 20 | 21 | _sysbeep () { 22 | osascript -e "beep" &>/dev/null 23 | } 24 | 25 | _beep () { 26 | afplay "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/system/Acknowledgement_ThumbsUp.caf" &>/dev/null 27 | } 28 | 29 | sysv=$(sw_vers -productVersion) 30 | sysmv=$(echo "$sysv" | awk -F. '{print $2}') 31 | if [[ $sysv != "11"* ]] ; then 32 | if [[ "$sysmv" -lt 15 ]] ; then 33 | _sysbeep & 34 | osascript &>/dev/null << EOT 35 | tell application "System Events" 36 | display notification "macOS 10.15 (Catalina) required!" with title "$process [" & "$account" & "]" subtitle "⚠️ Error: incompatible system!" 37 | end tell 38 | EOT 39 | echo -e "Error! Incompatible system.\n$scrname v$macsuv needs at least macOS 10.15 (Catalina).\n*** Exiting... ***" >&2 40 | exit 1 41 | fi 42 | fi 43 | 44 | icon_loc="/System/Library/PreferencePanes/Security.prefPane/Contents/Resources/FileVault.icns" 45 | 46 | _notify () { 47 | if [[ "$tn_status" == "osa" ]] ; then 48 | osascript &>/dev/null << EOT 49 | tell application "System Events" 50 | display notification "$2" with title "$process [" & "$account" & "]" subtitle "$1" 51 | end tell 52 | EOT 53 | elif [[ "$tn_status" == "tn-app-new" ]] || [[ "$tn_status" == "tn-app-old" ]] ; then 54 | "$tn_loc/Contents/MacOS/terminal-notifier" \ 55 | -title "$process [$account]" \ 56 | -subtitle "$1" \ 57 | -message "$2" \ 58 | -appIcon "$icon_loc" \ 59 | >/dev/null 60 | elif [[ "$tn_status" == "tn-cli" ]] ; then 61 | "$tn" \ 62 | -title "$process [$account]" \ 63 | -subtitle "$1" \ 64 | -message "$2" \ 65 | -appIcon "$icon_loc" \ 66 | >/dev/null 67 | fi 68 | } 69 | 70 | accountname=$(id -un) 71 | HOMEDIR=$(eval echo "~$accountname") 72 | 73 | # look for terminal-notifier (only on Yosemite and later) 74 | tn=$(command -v terminal-notifier 2>/dev/null) 75 | if ! [[ $tn ]] ; then 76 | tn_loc=$(mdfind \ 77 | -onlyin /Applications/ \ 78 | -onlyin "$HOMEDIR"/Applications/ \ 79 | -onlyin /Developer/Applications/ \ 80 | -onlyin "$HOMEDIR"/Developer/Applications/ \ 81 | -onlyin /Network/Applications/ \ 82 | -onlyin /Network/Developer/Applications/ \ 83 | -onlyin /AppleInternal/Applications/ \ 84 | -onlyin /usr/local/Cellar/terminal-notifier/ \ 85 | -onlyin /opt/local/ \ 86 | -onlyin /sw/ \ 87 | -onlyin "$HOMEDIR"/.local/bin \ 88 | -onlyin "$HOMEDIR"/bin \ 89 | -onlyin "$HOMEDIR"/local/bin \ 90 | "kMDItemCFBundleIdentifier == 'fr.julienxx.oss.terminal-notifier'" 2>/dev/null | LC_COLLATE=C sort | awk 'NR==1') 91 | if ! [[ $tn_loc ]] ; then 92 | tn_loc=$(mdfind \ 93 | -onlyin /Applications/ \ 94 | -onlyin "$HOMEDIR"/Applications/ \ 95 | -onlyin /Developer/Applications/ \ 96 | -onlyin "$HOMEDIR"/Developer/Applications/ \ 97 | -onlyin /Network/Applications/ \ 98 | -onlyin /Network/Developer/Applicationsv \ 99 | -onlyin /AppleInternal/Applications/ \ 100 | -onlyin /usr/local/Cellar/terminal-notifier/ \ 101 | -onlyin /opt/local/ \ 102 | -onlyin /sw/ \ 103 | -onlyin "$HOMEDIR"/.local/bin \ 104 | -onlyin "$HOMEDIR"/bin \ 105 | -onlyin "$HOMEDIR"/local/bin \ 106 | "kMDItemCFBundleIdentifier == 'nl.superalloy.oss.terminal-notifier'" 2>/dev/null | LC_COLLATE=C sort | awk 'NR==1') 107 | if ! [[ $tn_loc ]] ; then 108 | tn_status="osa" 109 | else 110 | tn_status="tn-app-old" 111 | fi 112 | else 113 | tn_status="tn-app-new" 114 | fi 115 | else 116 | tn_vers=$("$tn" -help | head -1 | awk -F'[()]' '{print $2}' | awk -F. '{print $1"."$2}') 117 | if (( $(echo "$tn_vers >= 1.8" | bc -l) )) && (( $(echo "$tn_vers < 2.0" | bc -l) )) ; then 118 | tn_status="tn-cli" 119 | else 120 | tn_loc=$(mdfind \ 121 | -onlyin /Applications/ \ 122 | -onlyin "$HOMEDIR"/Applications/ \ 123 | -onlyin /Developer/Applications/ \ 124 | -onlyin "$HOMEDIR"/Developer/Applications/ \ 125 | -onlyin /Network/Applications/ \ 126 | -onlyin /Network/Developer/Applications/ \ 127 | -onlyin /AppleInternal/Applications/ \ 128 | -onlyin /usr/local/Cellar/terminal-notifier/ \ 129 | -onlyin /opt/local/ \ 130 | -onlyin /sw/ \ 131 | -onlyin "$HOMEDIR"/.local/bin \ 132 | -onlyin "$HOMEDIR"/bin \ 133 | -onlyin "$HOMEDIR"/local/bin \ 134 | "kMDItemCFBundleIdentifier == 'fr.julienxx.oss.terminal-notifier'" 2>/dev/null | LC_COLLATE=C sort | awk 'NR==1') 135 | if ! [[ $tn_loc ]] ; then 136 | tn_loc=$(mdfind \ 137 | -onlyin /Applications/ \ 138 | -onlyin "$HOMEDIR"/Applications/ \ 139 | -onlyin /Developer/Applications/ \ 140 | -onlyin "$HOMEDIR"/Developer/Applications/ \ 141 | -onlyin /Network/Applications/ \ 142 | -onlyin /Network/Developer/Applications/ \ 143 | -onlyin /AppleInternal/Applications/ \ 144 | -onlyin /usr/local/Cellar/terminal-notifier/ \ 145 | -onlyin /opt/local/ \ 146 | -onlyin /sw/ \ 147 | -onlyin "$HOMEDIR"/.local/bin \ 148 | -onlyin "$HOMEDIR"/bin \ 149 | -onlyin "$HOMEDIR"/local/bin \ 150 | "kMDItemCFBundleIdentifier == 'nl.superalloy.oss.terminal-notifier'" 2>/dev/null | LC_COLLATE=C sort | awk 'NR==1') 151 | if ! [[ $tn_loc ]] ; then 152 | tn_status="osa" 153 | else 154 | tn_status="tn-app-old" 155 | fi 156 | else 157 | tn_status="tn-app-new" 158 | fi 159 | fi 160 | fi 161 | 162 | echo "***********************************************" 163 | echo "*** Starting macOS Security components scan ***" 164 | echo "***********************************************" 165 | echo "$process ($scrname v$macsuv)" 166 | echo "Executing user: $accountname ($account)" 167 | localdate=$(date) 168 | echo "Local date: $localdate" 169 | 170 | # check for cache directory 171 | cachedir="$HOMEDIR/.cache/macSU" 172 | if ! [[ -d "$cachedir" ]] ; then 173 | echo -e "macOS Security Updates initial run\nNo cache directory detected: creating..." 174 | if ! mkdir -p "$cachedir" &>/dev/null ; then 175 | _sysbeep & 176 | echo -e "Error creating cache directory: $cachedir\n*** Exiting... ***" >&2 177 | exit 1 178 | else 179 | echo -n "$macsumv" > "$cachedir/macsumv.txt" 180 | echo "Cache directory created" 181 | fi 182 | fi 183 | if ! [[ -f "$cachedir/macsumv.txt" ]] ; then 184 | if ! [[ -f "$cachedir/AppE-version.plist" ]] ; then 185 | find "$cachedir" -type f -exec rm -f {} \; 2>/dev/null 186 | fi 187 | echo -n "$macsumv" > "$cachedir/macsumv.txt" 188 | fi 189 | 190 | # list of components variables 191 | read -d '' macsulist <<"EOF" 192 | App Exceptions@/System/Library/CoreServices/CoreTypes.bundle/Contents/Library/AppExceptions.bundle/version.plist@AppE-version.plist@CFBundleShortVersionString@/System/Library/CoreServices/CoreTypes.bundle/Contents/Library/AppExceptions.bundle@/System/Library/CoreServices/CoreTypes.bundle/Contents/Library/AppExceptions.bundle/Exceptions.plist@none 193 | Compatibility Notification Data@/Library/Apple/Library/Bundles/CompatibilityNotificationData.bundle/Contents/version.plist@CND-version.plist@CFBundleShortVersionString@/Library/Apple/Library/Bundles/CompatibilityNotificationData.bundle@/Library/Apple/Library/Bundles/CompatibilityNotificationData.bundle/Contents/Resources/CompatibilityNotificationData.plist@none 194 | Core LSKD (kdrl)@/usr/share/kdrl.bundle/version.plist@kdrl-version.plist@CFBundleShortVersionString@/usr/share/kdrl.bundle@/usr/share/kdrl.bundle/lskd.rl@none 195 | Core Suggestions@/System/Library/PrivateFrameworks/CoreSuggestionsInternals.framework/Versions/A/Resources/Assets.suggestionsassets/version.plist@CS-version.plist@CFBundleShortVersionString@/System/Library/PrivateFrameworks/CoreSuggestionsInternals.framework@/System/Library/PrivateFrameworks/CoreSuggestionsInternals.framework/Versions/A/Resources/Assets.suggestionsassets/AssetData@none 196 | Gatekeeper@/private/var/db/gkopaque.bundle/Contents/version.plist@GK-version.plist@CFBundleShortVersionString@/private/var/db/gkopaque.bundle@/private/var/db/gkopaque.bundle/Contents/Resources/gkopaque.db@none 197 | Gatekeeper E@/private/var/db/gke.bundle/Contents/version.plist@GKE-version.plist@CFBundleShortVersionString@/private/var/db/gke.bundle@/private/var/db/gke.bundle/Contents/Resources/gk.db@none 198 | Incompatible Apps@/Library/Apple/Library/Bundles/IncompatibleAppsList.bundle/Contents/version.plist@IncApps-version.plist@CFBundleShortVersionString@/Library/Apple/Library/Bundles/IncompatibleAppsList.bundle@/Library/Apple/Library/Bundles/IncompatibleAppsList.bundle/Contents/Resources/IncompatibleAppsList.plist@BuildVersion 199 | KEXT Exclusions@/Library/Apple/System/Library/Extensions/AppleKextExcludeList.kext/Contents/version.plist@KE-version.plist@CFBundleShortVersionString@/Library/Apple/System/Library/Extensions/AppleKextExcludeList.kext@/Library/Apple/System/Library/Extensions/AppleKextExcludeList.kext/Contents/Resources/ExceptionLists.plist@none 200 | Malware Removal Tool@/Library/Apple/System/Library/CoreServices/MRT.app/Contents/version.plist@MRT-version.plist@CFBundleShortVersionString@/Library/Apple/System/Library/CoreServices/MRT.app@none@none 201 | TCC@/Library/Apple/Library/Bundles/TCC_Compatibility.bundle/Contents/version.plist@TCC-version.plist@CFBundleShortVersionString@/Library/Apple/Library/Bundles/TCC_Compatibility.bundle@/Library/Apple/Library/Bundles/TCC_Compatibility.bundle/Contents/Resources/AllowApplicationsList.plist@none 202 | XProtect@/Library/Apple/System/Library/CoreServices/XProtect.bundle/Contents/version.plist@XP-version.plist@CFBundleShortVersionString@/Library/Apple/System/Library/CoreServices/XProtect.bundle@none@none 203 | EOF 204 | # System Integrity Protection@/System/Library/Sandbox/Compatibility.bundle/Contents/version.plist@SIP-version.plist@CFBundleShortVersionString@/System/Library/Sandbox/Compatibility.bundle@none@none 205 | 206 | # check for plist backups 207 | while IFS='@' read -r cname cplpath cbname ckey cinfo cplpathalt ckeyalt 208 | do 209 | if ! [[ -f "$cachedir/$cbname" ]] ; then 210 | if [[ $cplpathalt != "none" ]] ; then 211 | ipldate=$(stat -f %Sc -t %F" "%T "$cplpathalt") 212 | else 213 | ipldate=$(stat -f %Sc -t %F" "%T "$cplpath") 214 | fi 215 | ixpversion=$(defaults read "$cplpath" "$ckey" 2>/dev/null) 216 | ! [[ $ixpversion ]] && ixpversion="n/a" 217 | if [[ $ckeyalt != "none" ]] ; then 218 | build=$(defaults read "$cplpath" "$ckeyalt" 2>/dev/null) 219 | ! [[ $build ]] && build="n/a" 220 | buildstr=" ($build)" 221 | else 222 | buildstr="" 223 | fi 224 | echo "Backing up $cname: $ixpversion$buildstr [$ipldate]" 225 | cp "$cplpath" "$cachedir/$cbname" 226 | else 227 | echo "$cname backup detected" 228 | fi 229 | done < <(echo "$macsulist" | grep -v "^$") 230 | 231 | # check for initial system data backups 232 | if ! [[ -f "$cachedir/sysv.txt" ]] ; then 233 | echo "Saving current system version: $sysv" 234 | echo -n "$sysv" > "$cachedir/sysv.txt" 235 | fi 236 | if ! [[ -f "$cachedir/sysbuildv.txt" ]] ; then 237 | sysbuildv=$(sw_vers -buildVersion) 238 | echo "Saving current system build version: $sysbuildv" 239 | echo -n "$sysbuildv" > "$cachedir/sysbuildv.txt" 240 | fi 241 | hwdata_raw=$(system_profiler SPHardwareDataType) 242 | hwdata=$(echo "$hwdata_raw" | grep "Boot ROM Version") 243 | if ! [[ -f "$cachedir/efiv.txt" ]] ; then 244 | efiv=$(echo "$hwdata" | awk '{print $4}') 245 | echo "Saving current EFI (Boot ROM) version: $efiv" 246 | echo -n "$efiv" > "$cachedir/efiv.txt" 247 | fi 248 | if ! [[ -f "$cachedir/ibridgev.txt" ]] ; then 249 | ibridgev=$(echo "$hwdata" | awk -F"[()]" '{print $2}' | awk -F"iBridge: " '{print $2}' | awk -F, '{print $1}') 250 | ! [[ $ibridgev ]] && ibridgev="n/a" 251 | echo "Saving current iBridge version: $ibridgev" 252 | echo -n "$ibridgev" > "$cachedir/ibridgev.txt" 253 | fi 254 | if ! [[ -f "$cachedir/rootless.conf" ]] ; then 255 | echo "Backing up rootless.conf" 256 | cp /System/Library/Sandbox/rootless.conf "$cachedir/rootless.conf" 257 | fi 258 | 259 | # curl databases on https://github.com/hoakleyelc/updates 260 | if ! [[ -d "$cachedir/tmp" ]] ; then 261 | mkdir -p "$cachedir/tmp" 2>/dev/null 262 | fi 263 | eclbaseurl="https://raw.githubusercontent.com/hoakleyelc/updates/master" 264 | securl="$eclbaseurl/sysupdates.plist" 265 | rcsec_tmp="$cachedir/tmp/sysupdates.plist" 266 | rm -f "$rcsec_tmp" 2>/dev/null 267 | echo "Trying to download sysupdates.plist..." 268 | curl -q -f -L -s --connect-timeout 30 --max-time 30 --retry 1 "$securl" -o "$rcsec_tmp" &>/dev/null 269 | rcsec="$cachedir/sysupdates.plist" 270 | if [[ -f "$rcsec_tmp" ]] ; then 271 | echo "Success!" 272 | rm -f "$rcsec" 2>/dev/null 273 | mv "$rcsec_tmp" "$rcsec" 2>/dev/null 274 | else 275 | echo "ERROR downloading sysupdates.plist!" >&2 276 | fi 277 | modelid=$(echo "$hwdata_raw" | grep "Model Identifier" | awk -F": " '{print $2}') 278 | modelname=$(echo "$modelid" | tr -d '[:digit:]' | sed 's/,$//') 279 | # modelnumber=$(echo "$modelid" | tr -d 'a-zA-Z') 280 | hwurl="$eclbaseurl/$modelname.plist" 281 | rchw_tmp="$cachedir/tmp/$modelname.plist" 282 | rm -f "$rchw_tmp" 2>/dev/null 283 | curl -q -f -L -s --connect-timeout 30 --max-time 30 --retry 1 "$hwurl" -o "$rchw_tmp" &>/dev/null 284 | echo "Trying to download $modelname.plist..." 285 | rchw="$cachedir/$modelname.plist" 286 | if [[ -f "$rchw_tmp" ]] ; then 287 | echo "Success!" 288 | rm -f "$rchw" 2>/dev/null 289 | mv "$rchw_tmp" "$rchw" 2>/dev/null 290 | else 291 | echo "ERROR downloading $modelname.plist!" >&2 292 | fi 293 | 294 | _version () { 295 | ver1="$1" 296 | ver2="$2" 297 | if ! [[ $ver1 ]] || ! [[ $ver2 ]] ; then 298 | echo "ERROR: incomplete input" >&2 299 | return 300 | fi 301 | 302 | ver1count=$(echo "$ver1" | grep -o "\." | wc -l) 303 | ver2count=$(echo "$ver2" | grep -o "\." | wc -l) 304 | if [[ $ver1count != "$ver2count" ]] ; then 305 | echo -e "ERROR: different formats\n$ver1 != $ver2" >&2 306 | return 307 | fi 308 | ((ver1count++)) 309 | vcounter=1 310 | 311 | major1=$(echo "$ver1" | awk -F\. '{print $1}') 312 | major2=$(echo "$ver2" | awk -F\. '{print $1}') 313 | if [[ $major1 -gt $major2 ]] ; then 314 | echo "greater" 315 | return 316 | elif [[ $major1 -lt $major2 ]] ; then 317 | echo "lesser" 318 | return 319 | fi 320 | if [[ $vcounter == "$ver1count" ]] ; then 321 | echo "same" 322 | return 323 | fi 324 | ((vcounter++)) 325 | 326 | minor1=$(echo "$ver1" | awk -F\. '{print $2}') 327 | minor2=$(echo "$ver2" | awk -F\. '{print $2}') 328 | if [[ $minor1 -gt $minor2 ]] ; then 329 | echo "greater" 330 | return 331 | elif [[ $minor1 -lt $minor2 ]] ; then 332 | echo "lesser" 333 | return 334 | fi 335 | if [[ $vcounter == "$ver1count" ]] ; then 336 | echo "same" 337 | return 338 | fi 339 | ((vcounter++)) 340 | 341 | patch1=$(echo "$ver1" | awk -F\. '{print $3}') 342 | patch2=$(echo "$ver2" | awk -F\. '{print $3}') 343 | if [[ $patch1 -gt $patch2 ]] ; then 344 | echo "greater" 345 | return 346 | elif [[ $patch1 -lt $patch2 ]] ; then 347 | echo "lesser" 348 | return 349 | fi 350 | if [[ $vcounter == "$ver1count" ]] ; then 351 | echo "same" 352 | return 353 | fi 354 | ((vcounter++)) 355 | 356 | majbuild1=$(echo "$ver1" | awk -F\. '{print $4}') 357 | majbuild2=$(echo "$ver2" | awk -F\. '{print $4}') 358 | if [[ $majbuild1 -gt $majbuild2 ]] ; then 359 | echo "greater" 360 | return 361 | elif [[ $majbuild1 -lt $majbuild2 ]] ; then 362 | echo "lesser" 363 | return 364 | fi 365 | if [[ $vcounter == "$ver1count" ]] ; then 366 | echo "same" 367 | return 368 | fi 369 | ((vcounter++)) 370 | 371 | minbuild1=$(echo "$ver1" | awk -F\. '{print $5}') 372 | minbuild2=$(echo "$ver2" | awk -F\. '{print $5}') 373 | if [[ $minbuild1 -gt $minbuild2 ]] ; then 374 | echo "greater" 375 | return 376 | elif [[ $minbuild1 -lt $minbuild2 ]] ; then 377 | echo "lesser" 378 | return 379 | fi 380 | if [[ $vcounter == "$ver1count" ]] ; then 381 | echo "same" 382 | return 383 | fi 384 | ((vcounter++)) 385 | 386 | pbuild1=$(echo "$ver1" | awk -F\. '{print $6}') 387 | pbuild2=$(echo "$ver2" | awk -F\. '{print $6}') 388 | if [[ $pbuild1 -gt $pbuild2 ]] ; then 389 | echo "greater" 390 | return 391 | elif [[ $pbuild1 -lt $pbuild2 ]] ; then 392 | echo "lesser" 393 | return 394 | fi 395 | if [[ $vcounter == "$ver1count" ]] ; then 396 | echo "same" 397 | return 398 | fi 399 | 400 | echo "Out of range" >&2 401 | } 402 | 403 | # check current EFI/iBridge versions 404 | counter=0 405 | while true 406 | do 407 | dictmodel=$(/usr/libexec/PlistBuddy -c "Print :$counter:MacModel" "$cachedir/$modelname.plist" 2>/dev/null) 408 | if [[ $dictmodel ]] ; then 409 | if [[ $dictmodel == "$modelid" ]] ; then 410 | break 411 | fi 412 | fi 413 | ((counter++)) 414 | done 415 | fulldict=$(/usr/libexec/PlistBuddy -c "Print :$counter" "$cachedir/$modelname.plist" 2>/dev/null) 416 | if [[ $fulldict ]] ; then 417 | efiv_current=$(echo "$fulldict" | awk -F"EFIversion$sysmv = " '{print $2}' | grep -v "^$") 418 | ibridgev_current=$(echo "$fulldict" | awk -F"iBridge$sysmv = " '{print $2}' | grep -v "^$") 419 | else 420 | efiv_current="n/a" 421 | ibridgev_current="n/a" 422 | fi 423 | 424 | logbody="" 425 | updated=false 426 | 427 | # check auxiliary components 428 | sysv_previous=$(cat "$cachedir/sysv.txt") 429 | if [[ $sysv_previous == "$sysv" ]] ; then 430 | echo "System: unchanged ($sysv)" 431 | else 432 | _beep & 433 | updated=true 434 | echo "System: UPDATED from $sysv_previous to $sysv" 435 | logbody="$logbody\nSystem: $sysv_previous > $sysv" 436 | echo -n "$sysv" > "$cachedir/sysv.txt" 437 | _notify "System" "$sysv_previous > $sysv" 438 | fi 439 | sysbuildv=$(sw_vers -buildVersion) 440 | sysbuildv_previous=$(cat "$cachedir/sysbuildv.txt") 441 | if [[ $sysbuildv_previous == "$sysbuildv" ]] ; then 442 | echo "System build: unchanged ($sysbuildv)" 443 | else 444 | _beep & 445 | updated=true 446 | echo "System build: UPDATED from $sysbuildv_previous to $sysbuildv" 447 | logbody="$logbody\nSystem build: $sysbuildv_previous > $sysbuildv" 448 | echo -n "$sysbuildv" > "$cachedir/sysbuildv.txt" 449 | _notify "System build" "$sysbuildv_previous > $sysbuildv" 450 | fi 451 | efiv=$(echo "$hwdata" | awk '{print $4}') 452 | efiv_previous=$(cat "$cachedir/efiv.txt") 453 | if [[ $efiv_previous == "$efiv" ]] ; then 454 | echo "EFI (Boot ROM): unchanged ($efiv)" 455 | else 456 | _beep & 457 | updated=true 458 | echo "EFI (Boot ROM): UPDATED from $efiv_previous to $efiv" 459 | logbody="$logbody\nEFI (Boot ROM): $efiv_previous > $efiv" 460 | echo -n "$efiv" > "$cachedir/efiv.txt" 461 | _notify "EFI (Boot ROM)" "$efiv_previous > $efiv" 462 | fi 463 | if [[ $efiv != "n/a" ]] ; then 464 | eficomp=$(_version "$efiv_current" "$efiv" 2>&1) 465 | if [[ $eficomp == "greater" ]] ; then 466 | echo "EFI (Boot ROM): a NEWER version is available: $efiv < $efiv_current" 467 | logbody="$logbody\nEFI (Boot ROM): out-of-date [available: $efiv_current]" 468 | elif [[ $eficomp == "same" ]] ; then 469 | echo "EFI (Boot ROM): the current version is installed" 470 | elif [[ $eficomp == "lesser" ]] ; then 471 | echo "EFI (Boot ROM): a newer version is already installed" 472 | logbody="$logbody\nEFI (Boot ROM): newer version already installed" 473 | else 474 | echo -e "ERROR comparing EFI (Boot ROM) versions!\n$eficomp" >&2 475 | fi 476 | fi 477 | ibridgev=$(echo "$hwdata" | awk -F"[()]" '{print $2}' | awk -F"iBridge: " '{print $2}' | awk -F, '{print $1}') 478 | ! [[ $ibridgev ]] && ibridgev="n/a" 479 | ibridgev_previous=$(cat "$cachedir/ibridgev.txt") 480 | if [[ $ibridgev_previous == "$ibridgev" ]] ; then 481 | echo "iBridge: unchanged ($ibridgev)" 482 | else 483 | _beep & 484 | updated=true 485 | echo "iBridge: UPDATED from $ibridgev_previous to $ibridgev" 486 | logbody="$logbody\niBridge: $ibridgev_previous > $ibridgev" 487 | echo -n "$ibridgev" > "$cachedir/ibridgev.txt" 488 | _notify "iBridge" "$ibridgev_previous > $ibridgev" 489 | fi 490 | if [[ $ibridgev != "n/a" ]] ; then 491 | ibridgecomp=$(_version "$ibridgev_current" "$ibridgev" 2>&1) 492 | if [[ $ibridgecomp == "greater" ]] ; then 493 | echo "iBridge: a NEWER version is available: $ibridgev < $ibridgev_current" 494 | logbody="$logbody\niBridge: out-of-date [available: $ibridgev_current]" 495 | elif [[ $ibridgecomp == "same" ]] ; then 496 | echo "iBridge: the current version is installed" 497 | elif [[ $ibridgecomp == "lesser" ]] ; then 498 | echo "iBridge: a newer version is already installed" 499 | logbody="$logbody\niBridge: newer version already installed" 500 | else 501 | echo -e "ERROR comparing iBridge versions!\n$ibridgecomp" >&2 502 | fi 503 | fi 504 | 505 | pldate=$(stat -f %Sc -t %F" "%T /System/Library/Sandbox/rootless.conf) 506 | if [[ $(md5 -q /System/Library/Sandbox/rootless.conf) == $(md5 -q "$cachedir/rootless.conf") ]] ; then 507 | echo "SIP Configuration: unchanged [$pldate]" 508 | else 509 | _beep & 510 | updated=true 511 | echo "SIP Configuration: rootless.conf UPDATED on $pldate" 512 | logbody="$logbody\nSIP Configuration (rootless.conf): $pldate" 513 | rm -f "$cachedir/rootless.conf" 2>/dev/null 514 | cp /System/Library/Sandbox/rootless.conf "$cachedir/rootless.conf" 2>/dev/null 515 | _notify "SIP Configuration" "$pldate" 516 | fi 517 | 518 | sysup=$(/usr/libexec/PlistBuddy -c "Print" "$cachedir/sysupdates.plist") 519 | 520 | # check main components 521 | while IFS='@' read -r cname cplpath cbname ckey cinfo cplpathalt ckeyalt 522 | do 523 | if [[ $cplpathalt != "none" ]] ; then 524 | pldate=$(stat -f %Sc -t %F" "%T "$cplpathalt") 525 | else 526 | pldate=$(stat -f %Sc -t %F" "%T "$cplpath") 527 | fi 528 | nxpversion=$(defaults read "$cplpath" "$ckey" 2>/dev/null) 529 | ! [[ $nxpversion ]] && nxpversion="n/a" 530 | if [[ $ckeyalt != "none" ]] ; then 531 | nxpbuild=$(defaults read "$cplpath" "$ckeyalt" 2>/dev/null) 532 | ! [[ $nxpbuild ]] && nxpbuild="n/a" 533 | nxpbuildstr=" ($nxpbuild)" 534 | else 535 | nxpbuildstr="" 536 | fi 537 | if [[ $(md5 -q "$cplpath") == $(md5 -q "$cachedir/$cbname") ]] ; then 538 | echo "$cname: unchanged ($nxpversion$nxpbuildstr) [$pldate]" 539 | else 540 | oxpversion=$(defaults read "$cachedir/$cbname" "$ckey" 2>/dev/null) 541 | ! [[ $oxpversion ]] && oxpversion="n/a" 542 | if [[ $ckeyalt != "none" ]] ; then 543 | oxpbuild=$(defaults read "$cplpath" "$ckeyalt" 2>/dev/null) 544 | ! [[ $oxpbuild ]] && oxpbuild="n/a" 545 | oxpbuildstr=" ($oxpbuild)" 546 | else 547 | oxpbuildstr="" 548 | fi 549 | _beep & 550 | updated=true 551 | echo "$cname: UPDATED from $oxpversion$oxpbuildstr to $nxpversion$nxpbuildstr [$pldate]" 552 | logbody="$logbody\n$cname: $oxpversion$oxpbuildstr > $nxpversion$nxpbuildstr [$pldate] ($cinfo)" 553 | _notify "$cname" "$oxpversion$oxpbuildstr > $nxpversion$nxpbuildstr [$pldate] " 554 | rm -f "$cachedir/$cbname" 2>/dev/null 555 | cp "$cplpath" "$cachedir/$cbname" 2>/dev/null 556 | fi 557 | if [[ $nxpversion != "n/a" ]] ; then 558 | skipcomp=false 559 | tonotify=true 560 | if [[ $cname == "Gatekeeper" ]] ; then 561 | sec_current=$(echo "$sysup" | awk -F"Gatekeeper = " '{print $2}') 562 | elif [[ $cname == "Gatekeeper E" ]] ; then 563 | tonotify=false 564 | sec_current=$(echo "$sysup" | awk -F"GatekeepDE = " '{print $2}') 565 | elif [[ $cname == "KEXT Exclusions" ]] ; then 566 | tonotify=false 567 | sec_current=$(echo "$sysup" | awk -F"KEXT$sysmv = " '{print $2}') 568 | elif [[ $cname == "Malware Removal Tool" ]] ; then 569 | sec_current=$(echo "$sysup" | awk -F"MRT = " '{print $2}') 570 | elif [[ $cname == "TCC" ]] ; then 571 | tonotify=false 572 | sec_current=$(echo "$sysup" | awk -F"TCC$sysmv = " '{print $2}') 573 | elif [[ $cname == "XProtect" ]] ; then 574 | sec_current=$(echo "$sysup" | awk -F"XProtect$sysmv = " '{print $2}') 575 | else 576 | skipcomp=true 577 | fi 578 | if ! $skipcomp ; then 579 | seccomp=$(_version "$sec_current" "$nxpversion" 2>&1) 580 | if [[ $seccomp == "greater" ]] ; then 581 | _sysbeep & 582 | echo "$cname: a NEWER version is available: $nxpversion < $sec_current" 583 | logbody="$logbody\n$cname: out-of-date [available: $sec_current]" 584 | $tonotify && _notify "$cname" "Out-of-date: v$sec_current available!" 585 | elif [[ $seccomp == "same" ]] ; then 586 | echo "$cname: the current version is installed" 587 | elif [[ $seccomp == "lesser" ]] ; then 588 | echo "$cname: a newer version is already installed" 589 | logbody="$logbody\n$cname: newer version already installed" 590 | else 591 | echo -e "ERROR comparing $cname version numbers!\n$seccomp" >&2 592 | fi 593 | fi 594 | fi 595 | done < <(echo "$macsulist" | grep -v "^$") 596 | 597 | # log results 598 | if [[ -d "$HOMEDIR/Library/Logs/local.lcars.macOSSecurityUpdates" ]] ; then 599 | rm -rf "$HOMEDIR/Library/Logs/local.lcars.macOSSecurityUpdates" 2>/dev/null 600 | fi 601 | logloc="$HOMEDIR/Library/Logs/local.lcars.macOSSecurityUpdates.log" 602 | if $updated ; then 603 | logbody=$(echo -e "$logbody" | grep -v "^$") 604 | logger -i -s -t "macOS Security Updates" "$logbody" 2>> "$logloc" 605 | else 606 | logbody="No recent system updates" 607 | logger -i -s -t "macOS Security Updates" "$logbody" 2>> "$logloc" 608 | fi 609 | 610 | exit 611 | --------------------------------------------------------------------------------