Extension Attribute: Turning App Auto-Patch Receipts Into Patch Evidence
CompleteOverview
An extension attribute that walks App Auto-Patch’s receipt directory, reads the per-title JSON each patch attempt leaves behind, and reports the results into Jamf Pro inventory — separated into what succeeded and what failed. It’s the evidence half of the Cyber Essentials patching program.
The problem: “deployed” is not “working”
App Auto-Patch does the hard part well. It discovers what patchable software is actually installed, keeps it current on a schedule, and doesn’t require you to maintain a package for every title.
What it doesn’t do is tell you, fleet-wide, whether it’s succeeding.
It writes a receipt per title under /Library/Management/AppAutoPatch/receipts/<label>/latest.json — version, timestamp, exit code, status. Locally. On each Mac. Which means the evidence exists on every device and is visible from none of them.
That gap matters more under a framework with a deadline. Cyber Essentials gives you 14 days to apply critical updates. Knowing that AAP is deployed to a device tells you nothing about whether Chrome actually patched on it, and a title that has been failing quietly for three weeks looks exactly like one that has been succeeding — until an auditor asks, or the unpatched thing gets exploited.
Silent partial failure is the realistic failure mode of any automated patching system. Not the system stopping — that you’d notice. One title, on some machines, failing every run.
What it does
for f in "$appAutoPatchReceiptsFolder"/*/latest.json; do
label="$(basename "$(dirname "$f")")"
version="$(jx "$f" version)"
timestamp="$(jx "$f" timestamp)"
exitCode="$(jx "$f" exitCode)"
patch_status="$(jx "$f" status)"
…
done
Each receipt becomes one line — label | version | timestamp | exitCode | status — and the output is grouped under Success: and Failure: headings.
Four decisions do the work:
Failures are separated, not just included. A flat list of 40 patched titles is something nobody reads carefully enough to spot the two that failed. Grouping means a smart group can target “has anything under Failure” and the answer is one glance rather than parsing a wall of text.
Status is derived when it’s missing. If a receipt has no status field, the exit code decides: zero is success, anything else is failure. Receipt formats change between versions, and an EA that returns nothing useful after a tool update is worse than one that degrades to something sensible.
Output is capped at 50 titles. Extension attributes land in every inventory record forever; an uncapped list on a developer’s Mac with a long software tail would bloat every recon.
A missing receipts folder reports explicitly, rather than returning empty. “AAP has never run here” and “AAP ran and patched nothing” are different problems, and an empty string can’t distinguish them — the same reason the privilege elevation attribute says so out loud too.
plutil -extract … raw reads the JSON rather than pattern-matching it with grep, so a title containing a brace or quote doesn’t corrupt the parse.
Outcome
Per-title patch evidence across the fleet, with failures surfaced as their own group rather than buried in a success list. Patch compliance became something with per-device, per-application proof behind it instead of an inference from “the tool is installed.”
Lessons Learned
Automation you can’t audit is a belief. The receipts already existed — App Auto-Patch was doing everything right and writing the proof to disk — and the entire gap between having that data and being able to act on it was one extension attribute. That’s a recurring shape in endpoint work: the hard engineering is usually already done by the tool, and the missing piece is making its output visible where decisions get made.