A device tells you it is fully patched. The Windows Update page is green, the update history has three entries from last Thursday, and Get-HotFix comes back clean. Your compliance report agrees. Everything agrees.
None of that is evidence. Every one of those signals is a record of a conversation the device had with an update service — and a conversation that never happened looks, in all of them, exactly like a conversation that returned nothing to do.
Microsoft publishes a way out of that trap and almost nobody uses it. Wsusscn2.cab is a signed offline catalogue you point the Windows Update Agent at locally. The device evaluates its own applicability against Microsoft's own metadata, with no WSUS, no Windows Update endpoint, and no network in the loop, and hands you back a list of what it is actually missing. It is the closest thing to ground truth that exists on a Windows box, and this article is about how to drive it and how to read the answer honestly.
Wsusscn2.cab is a Microsoft-signed cabinet of update metadata that lets the Windows Update Agent scan a machine with no network at all. Microsoft documents its contents narrowly — security updates, update rollups and service packs — so it is emphatically not a complete catalogue, and it carries no payloads. You drive it through Microsoft.Update.ServiceManager.AddScanPackageService, then search with ServerSelection = 3 (ssOthers) and the ServiceID the registration hands back. That ID is generated at registration time, so you cannot hard-code it. On the lab device the online evidence was worthless — 1,815 Event 26 entries reading "successfully found 0 updates" in the same log as 24 failed scans — while Get-HotFix showed 4 rows against a real package census of 199 Superseded / 121 Installed / 95 Staged. Diagnose with the catalogue first; the reflex fixes come later, if at all.
The problem: "found 0 updates" is not a compliance statement
Start where most people stop. The Windows Update client writes its own operational log, and on a managed device it writes a lot of it. Here is the whole histogram from the lab machine, a live Windows 11 Enterprise 25H2 box on build 26200.9168.
Read those two numbers together, because separately they are both misleading. Event 26 fires 1,815 times saying the scan succeeded and found nothing. That is the line every dashboard turns green on.
But Event 25 fires 24 times in the same log, which proves this device is capable of failing to reach a service. And when it fails that way, the next successful scan against a stale cached state emits an Event 26 that is textually identical to the one a genuinely patched device emits. The log cannot distinguish "nothing to install" from "nothing to install that this service knows about right now".
Now look at the other inventory people trust, on the same device, in the same session.
Four against four hundred and fifteen. That gap has a documented cause — Microsoft states plainly that Win32_QuickFixEngineering "returns only the updates supplied by Component Based Servicing (CBS)" and that MSI-supplied and Windows Update-site updates "aren't returned" — and it is worth a whole article of its own. For our purposes the point is narrower: both of those numbers describe what is present. Neither describes what is absent, and absence is the thing your patch report claims to measure.
SoftwareDistribution, clears catroot2, or runs sfc /scannow. Do not. A datastore reset destroys the update history and the cached scan state, which is the evidence that would have told you whether the device has ever successfully talked to a service. You will still not know what is missing, and now you cannot find out what happened. The offline scan reads the machine; it changes nothing. Run it first.Why it happens: you have three inventories and none of them is a catalogue
The confusion is structural, not accidental. A Windows device has three ways to tell you about updates, and all three answer a different question.
Win32_QuickFixEngineering, behind Get-HotFix, is a history of CBS-delivered QFE updates. It has no state column, no build number, and an InstalledOn that is midnight on every row, so it cannot even order two installs on the same day.
Get-WindowsPackage is servicing state. It is far richer — on the lab device it accounted for all 415 packages across Installed, Staged and Superseded — but it only knows about packages that have reached the component store. A patch the device was never offered does not appear as missing. It does not appear at all.
The Windows Update client log is a transcript. It records what a service said, when the device managed to ask. It is downstream of policy, of proxy, of scan scheduling, and of whatever the service happened to be approving that day.
What none of them holds is the other half of the equation: the set of updates that exist and apply to this machine. That lives in a catalogue, and every normal path to a catalogue runs over the network through infrastructure you are currently trying to rule out.
What Microsoft actually publishes
Microsoft's answer to this is a file. The Using WUA to Scan for Updates Offline page opens by stating that WUA can scan for security updates "without connecting to Windows Update or to a Windows Server Update Services (WSUS) server". Read that literally. Not "without the internet" — without an update service of any kind in the picture.
The file is Wsusscn2.cab. It is a cabinet signed by Microsoft, and it carries metadata only: the page is explicit that it does not contain the updates themselves, so anything the scan names you still have to obtain by other means. Before it will scan, WUA validates that the cab is signed by a valid Microsoft certificate.
The download is a permanent redirect, and the headers on it are themselves diagnostic. This is a metadata request only — no file was retrieved — taken on the day of writing.
Two things fall out of those four header lines. The first is scale: 635 MiB is a lot of metadata, which is why Microsoft attaches a warning to the scan itself. Offline cab scans, it says, can bring "higher than normal memory usage", and the documented advice is to allocate more memory before you start — more processors, a bigger pagefile.
The second is freshness. The catalogue was stamped on Patch Tuesday and was twelve days stale when I looked. Microsoft says new versions are released "periodically as security-related updates are released, removed, or revised", which in practice tracks the monthly cycle. Record that Last-Modified value alongside every scan result you produce, because it is the expiry date on your conclusion.
There is a third, less obvious point, and it is the reason this technique counts as ground truth rather than as another opinion. The applicability decision is made by the device, using the device's own component state, against Microsoft's metadata. It is not a server telling you what it thinks you need, and it is not filtered through an approval group, a deployment ring, a deferral window or a release pin. Every other answer you can get about missing patches has passed through at least one of those. This one has not, which is exactly why it is worth the 635 MiB.
The honest limits of the catalogue
This is where most write-ups of wsusscn2.cab go wrong, so let me be precise about what Microsoft does and does not say.
Microsoft publishes a positive classification list, in the KB titled A new version of the Windows Update offline scan file, Wsusscn2.cab, is available for advanced users. The file, it says, "contains metadata from the following classifications: security updates, update rollups, and service packs". Three classifications. That is the whole scope.
What Microsoft does not publish anywhere I can find is an exclusion list. So the honest statement is the inverse of the documented list, not a quoted denial: if a classification is not one of those three, do not expect it to be represented. That rules out drivers, Defender definition updates, Features on Demand payloads, feature-update enablement packages and anything classified simply as "Updates".
| Question about the catalogue | Documented position | What that means for your report |
|---|---|---|
| Which classifications are inside? | Stated: security updates, update rollups, service packs. | Treat the result as a security-patch verdict, not a full update verdict. |
| Are drivers included? | Not in the stated classification list. No explicit denial published. | A clean offline scan is not evidence the driver stack is current. |
| Does it carry the updates? | Stated: it does not. You must obtain them "through other means". | The scan names KBs. Fetching them is a separate, still-offline problem. |
| Is it signed? | Stated: signed by Microsoft, SHA-256 only, and WUA validates it before scanning. | Verify the signature yourself too, before the file goes near an isolated network. |
| Can I parse it directly? | Microsoft does not publish the schema, on the stated grounds that the internal format is "subject to change". | Anything that reads the cab directly is unsupported and will break. Use the API. |
| How current is it? | New versions released "periodically" as updates are released, removed or revised. | Capture Last-Modified with every result. A stale catalogue produces a confidently wrong clean bill of health. |
KB5054156 on it is the Windows 11 25H2 enablement package, and it is the reason a policy pin reading TargetReleaseVersionInfo = 24H2 is sitting under an installed DisplayVersion of 25H2. Feature-level drift is a different investigation with different tools. Do not let a green offline scan close it.How to verify: register the offline service and search it
The mechanism first, because the API reads oddly until you understand what it is doing. WUA does not have an "offline mode". What it has is a list of update services it can search, and a method that turns a cab file into a temporary entry on that list. You then run an ordinary search and tell the searcher to use that entry instead of a real one.
So the first diagnostic step is to look at the list before you touch it. That registration is registry-backed, and the parent key is worth knowing.
| Value | Meaning | What to look for |
|---|---|---|
DefaultService (on the parent key) | The service ID that ssDefault resolves to on this device. | On the lab device: 7971f918-a847-4430-9279-4a52d1efe18d, which is Microsoft Update. If this is a WSUS ID, a default-server scan is asking your WSUS, not Microsoft. |
| Subkey named as a service GUID | One subkey per registered update service. | Four on the lab device. A scan-package registration is what your offline scan adds here; it should not still be there tomorrow. |
RegisteredWithAU | Whether Automatic Updates is opted in to that service. | 1 on Microsoft Update, 0 on the two catalogue services. This is the flag that decides which service unattended scans use. |
CountryCode | Region stamped on the catalogue-style services. | GB on the lab device. Present on the store and flighting services, absent on Windows Update and Microsoft Update. |
Pending / LoadShedding subkeys | Agent bookkeeping, not services. | Do not count them when you enumerate services from the registry. Enumerate through the API instead. |
Better still, ask the API rather than the registry, because the API gives you the friendly names and the one property that matters here.
IsScanPackageService is the discriminator. It is False on all four here, and that is the baseline you compare against: after your scan it should be False on all four again. If a scan-package service is still sitting in that collection, something did not clean up.
855E8A7C-ECB4-4CA3-B045-1DFA50104289 as DCAT and 117cab2d-82b1-4b5a-a08c-4d62dbee7782 as STORE. The lab device calls the first one "Windows Store (Prod)", never registers the second at all, and adds an 8b24b027-... it calls "DCat Flighting Prod" that the sample has never heard of. Read service IDs at runtime. Do not carry them around in your scripts.The four lines that do the work
Microsoft's documented pattern is short enough to reproduce in full. AddScanPackageService registers the scan package as a service with WUA and hands back an IUpdateService interface, and that returned object carries the ID you search against.
Four properties, one method call, and every value in there is documented. Take them in order.
ServerSelection = 3 is ssOthers from the ServerSelection enumeration — ssDefault = 0, ssManagedServer = 1, ssWindowsUpdate = 2, ssOthers = 3. It is the only value that makes the searcher look at ServiceID at all; the reference page for the property states the non-Windows-Update site "is searched only if the value of the ServerSelection property is ssOthers."
The criteria string is the default from Microsoft's own sample: IsInstalled=0 and Type='Software' and IsHidden=0. Microsoft's own minimal offline example uses just IsInstalled=0. The longer form is better for a compliance question because it drops driver entries and anything a user has hidden. And if you want to see the supersedence chain rather than only the current top of it, set IncludePotentiallySupersededUpdates to true before searching.
$Searcher.Online = $false. That property controls whether the searcher goes to the network for a normal scan, and an offline-mode search against a real service just re-reads cached state — the same stale data that produced your 1,815 identical Event 26 lines. The wsusscn2.cab route is different in kind: it brings its own catalogue. Setting Online = $false is not an offline scan.Which binaries are actually doing this
Worth knowing, because it tells you where the memory goes and what has to be healthy. Every wuapi.h reference page names Wuapi.dll as the DLL, and on the lab device the COM registration confirms it: both class IDs resolve to an InProcServer32 in System32. Then watch what loads when you instantiate them.
Three files, three lessons. wuapi.dll in System32 is the COM surface — the registry has UpdateSession Class at {4CB43D7F-7EEE-4906-8698-60DA1C38F2FE} and UpdateServiceManager Class at {F8D253D9-89A4-4daa-87B6-1168369F0B21}, both pointing at it. It is versioned 1509.2607.1012.0, not 10.0.26200.x, because the update agent ships on its own train.
wuapicore.dll is the interesting one: it comes from C:\Windows\UUS\AMD64, the Unified Update Stack directory, the same place MoUsoCoreWorker.exe lives. So the API you are calling is not entirely an OS component; part of it is serviced independently of the build. wusys.dll is the one that still carries an OS-lineage version, 10.0.26100.8972.
And wuaueng.dll — the "Windows Update Agent" engine, the ServiceDll that wuauserv runs inside svchost.exe -k netsvcs — never appears in the calling process. That matters practically: the offline scan needs no network, but it still needs the Windows Update service to be startable. On a healthy idle device wuauserv sits at Running with start type Manual. If somebody has set it to Disabled as a "hardening" measure, fix that before you blame the cab.
The decision path
Numbered, so you can land on a cause rather than a feeling.
- Get the catalogue and record its identity. Note the
Last-Modifiedheader and hash the file. That is the evidence that dates your conclusion. - Verify the signature independently with
Get-AuthenticodeSignature -FilePath .\wsusscn2.caband confirmStatusisValidand the signer is Microsoft. WUA checks this too, but you want to know before the file crosses into an isolated network, not after. - Snapshot the service list as shown above. Confirm
IsScanPackageServiceisFalseeverywhere, and note theDefaultServicevalue so you know what a default scan would have hit. - Register and search using the four lines above, locally and elevated. If
AddScanPackageServicereturnsE_ACCESSDENIED, read the documented cause literally: "this method cannot be called from a remote computer." - If the count is zero, you have a clean bill of health for the three classifications in the catalogue, as of its Last-Modified date. Write it up that way. That sentence is the whole value of the exercise.
- If the count is non-zero, you have found real drift. Now the question becomes why the device was never offered these, which is a policy, scan-source or connectivity investigation — and you go into it knowing exactly which KBs are at stake.
- Compare against the online history. Missing KBs plus 1,815 "found 0 updates" events means the device has been having a successful-looking conversation with something that is not offering it these patches. That is a very different bug from a download failure.
- Clean up and re-snapshot the service list.
The fix: acting on a definitive list, and cleaning up after yourself
The scan itself is read-only and that is the point. What comes back is a list of titles and KB numbers, and here is the shape of it. This block is a rendering of the documented output format — it is not a capture from the lab device, which is fully patched and would return "There are no applicable updates."
Read the Categories line before you read the title. It tells you which of the three catalogued classifications produced this hit, which is exactly the caveat you need in the write-up. And read DeploymentAction, because a value of 3 means detection only — that entry is not something to install.
Then the actual remediation, which the catalogue deliberately does not help with: Microsoft states the cab does not contain the updates, so you fetch the named KBs from the Microsoft Update Catalog on a connected machine and carry them in. That is a media-handling problem, not a Windows Update problem, and it is the honest end of an offline diagnosis.
One practical note on getting the cab there in the first place. It is 635 MiB of file that has to cross whatever boundary makes the target isolated, which usually means removable media and a transfer nobody is watching closely. Hash the file on the connected side, hash it again on the isolated side, and compare before you run anything. WUA will refuse a cab whose signature does not validate, but a truncated copy that still happens to validate is a worse outcome than a refusal, because it produces a short answer that looks like a real one.
Removing the registration
The flags argument you passed decides whether you have work to do. Microsoft documents exactly one value for UpdateServiceOption: usoNonVolatileService = 0x1. Pass it, and you are obliged to call IUpdateServiceManager::RemoveService yourself — releasing the IUpdateService interface without doing so is documented to cause a "resource leak". Pass 0 instead and the registration is removed automatically when the interface is released.
There is a nice piece of archaeology here. Microsoft's current offline-scan sample calls AddScanPackageService with two arguments; the archived version of the same page passes a third argument of 1; and the larger sample script passes 0. All three are valid, and they behave differently. If you inherited a script from a blog post, check which one it uses and whether it calls RemoveService to match.
RemoveService will not let you dismantle the machine's real update configuration, and the error codes say so. It returns WU_E_DS_NEEDWINDOWSSERVICE if "the Windows Update service could not be removed" and WU_E_DS_INVALIDOPERATION if "the state of Automatic Updates could not be changed". If your cleanup code is hitting those, it is passing the wrong service ID — almost certainly a hard-coded one — and trying to unregister a real service instead of your scan package.| Return code | Where it comes from | What it actually means |
|---|---|---|
E_ACCESSDENIED | AddScanPackageService, RemoveService | "This method cannot be called from a remote computer." The scan is local, full stop — no PowerShell remoting shortcut. |
E_INVALIDARG | AddScanPackageService | A parameter value is invalid. In practice: a path that does not exist, or the wrong argument count for the overload you meant. |
WU_E_INVALID_OPERATION | AddScanPackageService | Documented twice on the same page: the computer could not access the update site, and the object implementing the interface has been locked down. |
A WinVerifyTrust error | AddScanPackageService | The signature check failed. Re-download the cab; do not work around this. |
WU_E_INVALID_CRITERIA | Search | Your criteria string is malformed. Note that OR is only legal at the top level, and != only works with the Type criterion. |
WU_E_DS_UNKNOWNSERVICE | RemoveService | You are removing something that was never registered. Usually a stale variable after a failed registration. |
ClientApplicationID on the session to something you will recognise. Microsoft's sample documents it as the name passed to WUA as the calling application, which "appears in the Windows Update logs". On this device the real traces are ETW .etl files under C:\Windows\Logs\WindowsUpdate\ — the C:\Windows\WindowsUpdate.log file is a 276-byte stub that just points you at Get-WindowsUpdateLog. And do not reach for Get-WindowsUpdateLog -ForceFlush mid-diagnosis: it stops the Update Orchestrator and Windows Update services.Proof it worked: the numbers from the lab device
Everything below was measured read-only on one live Windows 11 Enterprise 25H2 device, build 26200.9168, in a single session. The point of listing them together is that they disagree, and that the disagreement is the finding.
The event channel is worth cataloguing properly, because these four IDs are the entirety of what this device's Windows Update client has ever reported, and three of the four are routinely over-read.
| Event ID | Message | What it tells you |
|---|---|---|
| 25 (24 entries) | Windows Update failed to check for updates with error 0x80240438. | The scan never reached a service. Note that 0x80240438 is not enumerated in Microsoft's published Windows Update error list, so do not assert a meaning for it — treat it as "scan did not complete" and look at the transport. |
| 26 (1,815 entries) | Windows Update successfully found 0 updates. | Nothing. A fully patched device and a device excluded by a stale pin emit this identical line. It is the single most over-trusted event in Windows patching. |
| 31 (2 entries) | Windows Update failed to download an update. | Something was offered and the transfer failed. Rare here, and it is a BITS or Delivery Optimization question, not a catalogue question. |
| 41 (335 entries) | An update was downloaded. | Content actually arrived. Pair it with the Setup log to see whether it then reached the Staged or Installed state. |
Set that against the three inventories. Get-HotFix: 4 rows. Get-WindowsPackage: 415 packages, in three states, 199 of them already superseded. The offline catalogue available to check any of it against: 635 MiB, stamped 11 August 2026, twelve days old.
Four numbers, four different pictures of the same machine, and only one of them — the one nobody runs — is capable of answering "what is missing". That is the whole argument for adding an offline scan to your diagnostic kit, and for putting it before the fix rather than after it.
What a good result looks like when you write it up: the catalogue's Last-Modified date and hash, the signer and status from Get-AuthenticodeSignature, the service ID that was generated and then removed, the exact criteria string, the ClientApplicationID you set, and the count. With those seven facts recorded, somebody else can reproduce your conclusion in six months. Without them you have an anecdote.
And the last check is the cheapest one. Re-enumerate $sm.Services. If it is back to four entries with IsScanPackageService = False on all of them, you scanned the machine, learned something true, and left it exactly as you found it. That is what a diagnostic is supposed to do.
References
- Using WUA to Scan for Updates Offline - the primary source. The download link, the "signed by Microsoft" and "doesn't contain the security updates themselves" statements, the SHA-256-only signing note, the high-memory warning, and both the VBScript and PowerShell samples.
- IUpdateServiceManager::AddScanPackageService - the full signature including the flags parameter, the
E_ACCESSDENIED"cannot be called from a remote computer" rule, theWinVerifyTrustbehaviour, theIsScanPackageServiceproperty, and theWuapi.dllrequirement. - UpdateServiceOption enumeration -
usoNonVolatileService = 0x1, the resource-leak warning, and the automatic-removal behaviour when you omit it. - IUpdateSearcher::ServiceID - the rule that the ID is only honoured when
ServerSelectionisssOthers. - ServerSelection enumeration - the four members and their values, and the sentence tying
ssOthersto theServiceIDproperty. - IUpdateSearcher::Search - the complete criteria table, the operator restrictions, the default criteria, and
WU_E_INVALID_CRITERIA. - Searching, Downloading, and Installing Updates - the larger Microsoft sample: the
/Offlineswitch,AddScanPackageService(..., 0), the service-ID constants,ClientApplicationID, and theDeploymentActionmapping used above. - IUpdate interface - every property you can read off a result, including
KBArticleIDs,Categories,MsrcSeverity,SupersededUpdateIDsandLastDeploymentChangeTime. - IUpdateServiceManager::RemoveService - the cleanup call and its
WU_E_DS_NEEDWINDOWSSERVICE/WU_E_DS_INVALIDOPERATION/WU_E_DS_UNKNOWNSERVICEguardrails. - A new version of the Windows Update offline scan file, Wsusscn2.cab, is available for advanced users - the classification list quoted above, and the statement that Microsoft does not publish the schema because the internal format is subject to change.
- Get-HotFix - the notes section:
Win32_QuickFixEngineering"returns only the updates supplied by Component Based Servicing (CBS)", and MSI or Windows Update site updates "aren't returned". - Get-AuthenticodeSignature - the independent signature check for step 2, and the
Statusproperty you filter on.