A bonus follow-up post regarding software update tracking. At the recommendation of @Chris in the MacAdmins Slack, and with assistance from @Pico, I've put together a fact that aggregates many of the individual checks from my previous posts into a more monolithic fact/extension attribute. Here's the logic flow:
Get the currently-installed version of macOS
Determine the most recently published update of all major macOS versions (this is for updates to the current OS, not upgrades to the latest major release)
Determine what updates the Mac thinks are available
Throw an error flag if the Mac thinks there are no updates available, but isn't running the most recently published update
The primary difference between this and my previous iterations of similar facts comes in the use of this webpage to update the definitions for the newest available macOS patches. By doing a curl of this page and then some cleanup of the output, you can get retrieve a list of the newest updates without having to manually update the script each month. A quick disclaimer, I did all my parsing of the output of that page with some pretty ugly sed and awk commands. In your environment, you can definitely clean up this process by replacing the string manipulation with JSON parsing.
The biggest benefit of using an external data source that Apple updates (so we don't have to) is that this simple curl is much faster and lighter weight than any query involving softwareupdate. Every MDM platform will have a different cadence for when scripts like these will be run, so our goal is to put as little pressure on that fragile little binary as possible.
Before we break it down, here's the whole script:
Let's take this apart one block at a time.
Lines 8-19: Current OS
This is all very straightforward - we snag the current version of macOS on the device (the full version number and the major version number), give our most important device state variables placeholder values, and bail out if the fact is running on macOS 10.15 or earlier. In order to not act on older, incompatible operating systems later, we just exit with false and call it a day.
Lines 21-41: Most recent macOS releases
For your own understanding, if you want a less horrific view of the gdmf webpage, run it through a JSON cleaner. The contents of the page are a few dictionaries that include the version numbers, release date, expiration date, and supported devices for macOS, iOS, iPadOS, and watchOS releases. Eagle-eyed admins will even catch the recently released Rapid Security Response (RSR) updates posted on May 1st, 2023, down at the bottom.
What we really care about though, are the first few levels of the page, PublicAssetSets/macOS, which contains the newest versions of Big Sur, Monterey, and Ventura, all the versions of macOS that are currently receiving updates/patches:
Since the document is formatted with the newest builds up at the top, it allows us to easily scrape the first version string we find for each major OS, clean it up, and set our variables. I left the logic behind the truncation necessary to clean up the curl for you to peruse, if helpful. In summary, it's ugly but it works. It'd be much more productive to manipulate the JSON itself to pull the ProductVersion values we want, but this gets the job done for now.
Lines 43-62: skipStrings
# Set Upgrade names to skip - delta upgrades started in macOS 12, Monterey
# Add macOS 14 to the skipStrings for 12 & 13 upon release
if [ "$currentOSMajor" = 13 ]; then
#echo "13 - Ventura"
# No skip strings yet, macOS 14 not named
skipString=("null")
elif [ "$currentOSMajor" = 12 ]; then
#echo "12 - Monterey"
skipString=("Ventura")
# The below OS versions shouldn't need skipstrings
elif [ "$currentOSMajor" = 11 ]; then
#echo "11 - Big Sur"
# No skip strings, deltas not supported
skipString=("null")
elif [ "$currentOSMajor" = 10 ]; then
#echo "10.15 - Catalina or earlier"
# No skip strings, deltas not supported
skipString=("null")
fi
I went through this logic in one of my earlier posts - this is one of the only things you'll need to update semi-regularly about this script (assuming Apple doesn't perform any significant refactors of the site we took our info from). Now that macOS upgrades can be served as delta updates, they can appear in the softwareupdate binary's checks for available updates. We use the skipString arrays to detail the names of major operating system versions that may appear in the list of available updates so those lines can be ignored. Since the delta upgrade feature was introduced in macOS Monterey, we don't need skipString values for Big Sur or earlier (I left macOS 11 and 10.x in there as reminders, feel free to delete those lines). When macOS 14 is launched, its full name will need to be added to the Monterey and Ventura skipString arrays.
Lines 66-110: Up-to-date check
Now that we have the version numbers for the most recent builds, we can compare those to the currently-installed version, and determine if the current Mac is up-to-date. Remember, the logic of this script is all about targeting the latest version of the currently-installed OS, not upgrading to the latest major OS version. On the off-chance a Catalina-or-earlier Mac made it this far, we have another safety bailout. We'll store our results in a boolean to be called later.
Lines 112-130: How many updates are available?
There's a .plist file that stores useful information from your last softwareupdate check - we'll query that file and get a list of the names of the available software updates. With those items listed, we then remove any updates that have the skipString value for newer major OS versions in them. If the list ends up empty, meaning either there are no updates visible or only delta upgrades visible, we set the noUpdatesVisible variable to true. This means the Mac believes it is fully updated.
Lines 132-161: Assess state
Using all of the information we've gleaned thus far, a final assessment of the device state is made. We're looking at all of the possible intersections of two variables, noUpdatesVisible and upToDate. In the Punnett square of results, only one quadrant results in a device state we'd consider "bugged" - if no updates are visible to the Mac, but the Mac is verified not to be running the latest patch.
With this information in hand, it's up to you to decide how you want to remediate. I've got this rigged up so any device that's in the "bugged" state (returns true in the above script) will have the softwareupdate binary kickstarted. Remember, Apple actively recommends against routinely kickstarting this binary as a proactive remediation measure. Scripts like this allow us to target devices with more specificity, only interacting with those that really need the extra push.