ERROR_SXS_ASSEMBLY_MISSING (0x80073701) on Windows Server 2019

We’ve run into a strange problem with our Windows Server 2019 VMs where sometimes when we clone a new VM from our template it works perfectly fine and sometimes it won’t let you install any new Roles and throws a 0x80073701 error. Better still, sometimes it lets you install a new Role and then months later when you go to add something else it fails also with a 0x80073701.

For the longest time the only solution I was able to find online was nuke it and start over which is typically what we did. That or to manually dig through registry keys for packages installed in Windows with a different language than that of your operating system. I never was able to get that suggestion to work because I couldn’t gain the permissions I needed to delete the registry keys. We also had zero luck running DISM with it’s variety of flags.

At some point a co-worker of mine stumbled across a PowerShell script that solved the problem for us and saved us having to rebuild a few more complicated VMs.

I had to use that script tonight on a VM but this time it didn’t work. I tried to see if I could find a new version but I couldn’t even find the original script. With some fiddling I eventually got the script to work but it dawned on me that lots of people might have having this problem and the script to fix it with out wiping/reloading might not be easily found anymore.

Full Disclosure. I did not write this script, I’ve only used it a few times with success. I searched for the authors name to see if they had a Github repo or something out there and found nothing other than a LinkedIn.

Near as I can tell this script does the following:

  1. Elevates its privileges in a very specific looking way. I did not dig much into it since the rest of the script does not appear to do anything malicious and you should run this “As an administrator” anyway I just went with it
  2. It then asks you for the location of your CBS log file, if none is provided it uses the default location
  3. It then parses the CBS log file for any instances of ERROR_SXS_ASSEMBLY_MISSING and then parses those lines to pull out the specific package names that are causing problems
  4. Using it’s elevated privileges it takes ownership of that packages registry keys and changes the ‘Currentstate’ key to ‘0’ which I assume means not installed or ignored
  5. It does some checks to make sure the ‘Currentstate’ was successfully changed and then completes

Once the script has run you do not need to reboot. You should be able to start adding Roles to the server right away.

I have found that running the script against “C:\Windows\Logs\CBS\CBS.log” does not always solve the problem. Tonight I went into “C:\Windows\Logs\CBS\” and had to run it against the second newest log “CbsPersist_20230310065917.log”. After doing that the issue was resolved for me.

In our case it appears the issue is with KB4598230 which has been pulled from the Microsoft Update Catalogue and can no longer be downloaded. I have seen plenty of form posts involving other KBs causing the exact same error though.

Sorry, that was a lot of reading. Here is what you are after:

  This script will fix the SXS assmbly missing issue while installing feature

  The script mark the resolved packages absent which are missing manifest.


    Provide CBS file path

  <Outputs if any, otherwise state None - example: Log file stored in current working directory  "AssemblyMissingScript-" + [datetime]::Now.ToString("yyyyMMdd-HHmm-ss") + ".log")>
  Version:        1.0
  Author:         Abhinav Joshi
  Creation Date:  14/11/2020
  Purpose/Change: Initial script development

  Please enter CBS file path (Default Path: c:\windows\logs\cbs\cbs.log): C:\windows\Logs\cbs\cbs2.log

function enable-privilege {
        ## The privilege to adjust. This set is taken from
            "SeAssignPrimaryTokenPrivilege", "SeAuditPrivilege", "SeBackupPrivilege",
            "SeChangeNotifyPrivilege", "SeCreateGlobalPrivilege", "SeCreatePagefilePrivilege",
            "SeCreatePermanentPrivilege", "SeCreateSymbolicLinkPrivilege", "SeCreateTokenPrivilege",
            "SeDebugPrivilege", "SeEnableDelegationPrivilege", "SeImpersonatePrivilege", "SeIncreaseBasePriorityPrivilege",
            "SeIncreaseQuotaPrivilege", "SeIncreaseWorkingSetPrivilege", "SeLoadDriverPrivilege",
            "SeLockMemoryPrivilege", "SeMachineAccountPrivilege", "SeManageVolumePrivilege",
            "SeProfileSingleProcessPrivilege", "SeRelabelPrivilege", "SeRemoteShutdownPrivilege",
            "SeRestorePrivilege", "SeSecurityPrivilege", "SeShutdownPrivilege", "SeSyncAgentPrivilege",
            "SeSystemEnvironmentPrivilege", "SeSystemProfilePrivilege", "SeSystemtimePrivilege",
            "SeTakeOwnershipPrivilege", "SeTcbPrivilege", "SeTimeZonePrivilege", "SeTrustedCredManAccessPrivilege",
            "SeUndockPrivilege", "SeUnsolicitedInputPrivilege")]
        ## The process on which to adjust the privilege. Defaults to the current process.
        $ProcessId = $pid,
        ## Switch to disable the privilege, rather than enable it.
        [Switch] $Disable

    ## Taken from P/Invoke.NET with minor adjustments.
    $definition = @'
 using System;
 using System.Runtime.InteropServices;
 public class AdjPriv
  [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
  internal static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
   ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
  [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
  internal static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok);
  [DllImport("advapi32.dll", SetLastError = true)]
  internal static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);
  [StructLayout(LayoutKind.Sequential, Pack = 1)]
  internal struct TokPriv1Luid
   public int Count;
   public long Luid;
   public int Attr;
  internal const int SE_PRIVILEGE_ENABLED = 0x00000002;
  internal const int SE_PRIVILEGE_DISABLED = 0x00000000;
  internal const int TOKEN_QUERY = 0x00000008;
  internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
  public static bool EnablePrivilege(long processHandle, string privilege, bool disable)
   bool retVal;
   TokPriv1Luid tp;
   IntPtr hproc = new IntPtr(processHandle);
   IntPtr htok = IntPtr.Zero;
   retVal = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok);
   tp.Count = 1;
   tp.Luid = 0;
   retVal = LookupPrivilegeValue(null, privilege, ref tp.Luid);
   retVal = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
   return retVal;

    $processHandle = (Get-Process -id $ProcessId).Handle
    $type = Add-Type $definition -PassThru
    $type[0]::EnablePrivilege($processHandle, $Privilege, $Disable)

$logfile = [System.IO.Path]::Combine($rootDir, "AssemblyMissingScript-" + [datetime]::Now.ToString("yyyyMMdd-HHmm-ss") + ".log")
if (-not (Test-Path "$PWD\logs")) {
    New-Item -Path "$PWD\logs" -ItemType Directory -Verbose
Start-Transcript -Path "$PWD\logs\$logfile"

$cbspathTEMP = Read-Host -Prompt "Please enter CBS file path (Default Path: c:\windows\logs\cbs\cbs.log)"

$cbspath = $cbspathTEMP.Replace('"','')

write-host  ""

write-host -ForegroundColor Yellow $cbspath

if ($cbspath -eq $null -or $cbspath.Length -eq "0"){
    Write-Host -ForegroundColor Yellow "No path was entered"
    Write-Host "Setting up default CBS path"

    $cbspath = "c:\Windows\Logs\CBS\CBS.log"

    Write-Host -ForegroundColor Cyan $cbspath

$CheckingpackagesResolving = "Resolving Package:"

$checkingFailure = Get-Content $CBSpath | Select-String "ERROR_SXS_ASSEMBLY_MISSING"

    if ($checkingFailure -ne $null -and $CheckWhichFeature -ne 0) {

            Write-Host "Checking resolving packages"

            $CBSlines = Get-Content $CBSpath | Select-String $CheckingpackagesResolving

            $Result = @()

            if ($CBSlines) {

                foreach ($CBSline in $CBSlines) {

                    $packageLine = $CBSline | Out-String

                    $package = $packageLine.Split(":").Trim().Split(',').Trim() | Select-String "Package_"

                    $Result += $package

                Write-host "Found following resolving packages"

                $Results = $Result | Select-Object -Unique

                foreach ($regpackage in $Results) {

                    $bb = "SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\Packages\$regpackage"

                    $uname = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name

                    enable-privilege SeTakeOwnershipPrivilege 

                    $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($bb, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, [System.Security.AccessControl.RegistryRights]::takeownership)
                    # You must get a blank acl for the key b/c you do not currently have access
                    $acl = $key.GetAccessControl([System.Security.AccessControl.AccessControlSections]::None)
                    $me = [System.Security.Principal.NTAccount]$uname

                    # After you have set owner you need to get the acl with the perms so you can modify it.
                    $acl = $key.GetAccessControl()
                    $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($uname, "FullControl", "Allow")


                    Write-Host "Mark this package absent $regpackage"

                    Set-ItemProperty -Path "HKLM:\$bb" -Name Currentstate -Value 0 -Type DWord -Force

                Write-host "Verifying package state"

                $Verifcationcheckvalue = "1"

                foreach ($Regpackagecheck in $Results) {
                    $CurrentstateOfpackage = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\Packages\$Regpackagecheck").CurrentState
                    if ($CurrentstateOfpackage -eq "0") {
                        Write-host -ForegroundColor Green  $CurrentstateOfpackage of $Regpackagecheck

                        $Verifcationcheckvalue += "1"

                    else {

                        Write-host -ForegroundColor red  $CurrentstateOfpackage of $Regpackagecheck

                        $Verifcationcheckvalue += "0"

                if ($Verifcationcheckvalue -notmatch "0") {
                    write-host "========================================================================="

                    write-host ""

                    Write-host -f white -BackgroundColor green "Verification passed, Retry Enabled"

                    write-host ""

                    write-host "========================================================================="

                    $Global:try = $true

                else {

                    write-host "========================================================================="

                    write-host ""

                    write-host -f white -BackgroundColor Red "Verification Failed, Can't contiune. Collect $logfile and CBS.log"

                    write-host ""

                    write-host "========================================================================="

                    $Global:try = $false

            else {

                Write-Error "Error while finding resolving packages"

    else {

            Write-Host "Looks like $CBSpath is not right CBS File, check manually. "




Abhinav Joshi, whoever you are. Thank you very much for this script. It’s saved us a ton of time and headache.

Does the Purify ad blocker for iOS still get updates?

TL;DR – The app still works but the block list has not been updated since 2018-10-20.

I’ve had the Purify App installed on my iOS devices since it was originally released and have recently been wondering if it still worked. The last version update for the App was 2018-09-21 which honestly is probably fine since blocking content in a web browser is probably a solved problem at this point and all that really matters is that the block list itself is being updated.

When you launch Purify on your device and click ‘Update Now’ it appears to be checking and pulling the latest block list right? I had assumed so.

Today I decided to check. I grabbed a copy of mitmproxy, installed it’s SSL certificate on a test device and started poking around. Here is what I found.

When you click the ‘Update Now’ button the App reaches out to:

The first number (1667683942) is a Unix timestamp for right now. I have no idea what the second number and characters (93690300313600ACFB) is for. It changes every time I click ‘Update Now’.

Either way you don’t need either value to view the contents of which is just a text file named “latest-version” that contains a number (105 as of this writing).

The number 105 signifies the latest revision of the block list Purify uses. If you already have this version download the Purify App does nothing further.

Using mitmproxy I was able to intercept and alter the response from the web server so that it returned “106” instead of “105” which triggered the App into thinking there was a new block list to download. I then had what I was after which was the URL of the block list itself.

The 105 version of the block list can be found here:

If you change 105 to 104 you’ll get a previous version of the block list.

Checking the headers on the web request I found the “last-modified” stated “Sat, 20 Oct 2018 17:35:43 GMT”.

Mozilla says:

The Last-Modified response HTTP header contains a date and time when the origin server believes the resource was last modified.

This leads me to believe the block list for Purify hasn’t been updated since 2018-10-20 making the App fairly useless at this point despite likely still functioning.

Before publishing this blog post I reached out to the author for comment but heard nothing back after waiting a week.

If you’ve somehow stumbled across this blog post before purchasing Purify, I’d recommend spending your $1.99 elsewhere. It’s a little disappointing the developer hasn’t pulled the App from the store where it can still be purchased or published anything on the official Website or Twitter feed stating the block list is no longer being updated.

Do I have a recommendation for an alternative? Not an iOS App specifically. I use a PiHole personally.

MikroTik CSS326 fan installation

It was time to upgrade my networking equipment in my homelab. I needed two 24p 1GB switches with 10GB uplinks to facilitate moving my homelab into my crawl space and out of my office. As things are configured right now I’d easily saturate a 1GB uplink between the switches and since I don’t have any 10GB in my homelab yet the CSS326 fits the bill. I am replacing a single first generation Ubiquiti 24p switch with two CSS-326-254G-2S+RM’s.

After receiving my CSS-326-254G-2S+RM’s I plugged them in to test them and verify my 10GB SFP+ transceivers were working properly. While I was doing that I also hooked them into PRTG using SNMP so see what kind of monitoring data I could pull from them. I had been optimistic that these new switches would run cooler than my Ubuiqiti but that does not seem to be the case. My Ubiquiti (which has a fan) runs at about 76c. The MikroTik ran at about 70c without a fan. An improvement but not a fantastic one.

Below is 1 hour of monitoring data from the MikroTik with a single 10GB SFP+ installed in it running idle but connected to my Ubiquiti via 1GBe and the other CSS-326 via fiber.

CSS-326-254G-2S+RM – 1 hour – CPU Temperature – Average 70c
CSS-326-254G-2S+RM – 1 hour – SFP+ Temperature – Average 43c

The CSS-326-254G-2S+RM has a mount for a 40mm fan and figured I’d just buy a fan and slap it in there. Unfortunately once I popped it open I saw there was no header to connect a fan to. I did some digging and came across this video which shows a significant temperature improvement once a fan is installed and the creator helpfully pointed out where you could tap into the switches board to get power. I also found this helpful forum post that also showed the J2 header and mentioned which connector was positive (+) and which was negative (-).

“YOU DON’T NEED TO DO THIS. Mine runs in a warm rack enclosure and has for 3 years now”

– Someone I know

Using my multimeter I checked the J2 connector and it outputs 24v a few seconds after the switch boots up. From what I have read, the J2 only outputs power if you connect the included power cable to your CSS-326. If you use PoE to power the CSS-326 the J2 connector does not output any power. I did not test this but since I’m using the supplied power cables this isn’t a problem for me.

Noctua is my preferred fan manufacturer but they do not make a 24v 40mm fan which means I had to use a buck converter to drop 24v down to 12v for the Noctua NF-A4x20 I wanted to use. I will put a full parts list at the bottom of this blog post.

I did not solder to the J2 connector as my first step. I just want to show where it is before continuing. Please excuse my poor soldering skills. This is only my 3rd or 4th time seriously soldering something and my first time soldering to a PCB. Using the information I gathered, I am labelling which connectors I treated as positive (+) and negative (-). I might be wrong but it all worked in the end.

J2 before I soldered my wires
J2 after I soldered my wires

Easy step first, I mounted the fan into the CSS-326 using some M3x12mm screws, nuts and a little patience.

I then 3D printed a baffle to block off the dead space to the right of the fan if you’re looking at the switch from the front. The electrical tape is just to hold the baffle in place while I’m fiddling inside the switch. Once you put the top back on the baffle is firmly pinched in place and won’t move. As designed the baffle is overkill. It could be 3mm thinner but I don’t care about saving 6g of filament so I didn’t change it for the second switch. The STL is linked at the bottom of this post in the parts list.

Using the cables and adapters that came with the Noctua fan I was able to piece things together in a way that I would never need to touch the J2 connector again if something failed. The buck converter can be detached from the main feed connected to the J2 and the fan can be disconnected from the buck converter. If either piece ever dies it should be very simple to replace them. I specifically used the extension cable and the Y-splitter. You can toss the one labelled “Low-Noise Adapter” into your spare parts bin, we won’t be needing it.

I have labelled each connector with a number so you can see how I piece things together

I removed all of the sheathing from the cables, removed any blue/green wires because I only need the yellow and black ones and then cut off some of the connectors.

Piecing them all together they will look like this:

Numbers in brackets mean a cut

(1) – Is the small connector on the extension cable that gets removed and soldered to J2

2 – Is the large connector on the extension cable that does NOT get removed. The IN on the buck connector will plug into this.

(3) – Is the large connecter on the upper leg of the Y-splitter that gets removed and soldered to the IN on the buck converter

4 – Is the large connector on the lower leg of the Y-splitter that does NOT get removed. The fan will plug into this which is attached to the OUT on the buck connector.

(5) – Is the small connector at the base of the Y-splitter. You want to cut this so that the smaller connector remains attached to the upper leg of the Y-splitter that you removed the large connector (3) from. This then gets soldered to the OUT on the buck converter

6 – Is the fans connector, leave it alone. You will plug it into 4 when everything is done

Before soldering (1) to the J2 connector feed it through the gap in baffle and under the mainboard so you can keep it all tucked out of the way. If you don’t do this first you’ll have to remove the mainboard and baffle to do it later. There should be just enough wire to make it to the J2.

Solder it all together based on the diagram above and plug everything in except for the fan. We need to adjust the buck converter before we can plugin the fan. Odds are its default setting is too high (more than 12v) for our Noctua.

Set your multimeter for DCV at whatever setting can read higher than 20v, connect alligator clips to the OUT side of the buck converter and then connect those to your multimeter. Plug the power into the switch and check your multimeter reading. Using a flathead screw driver carefully turn the small knob on top of the blue box on the buck converter until your multimeter reads 12v.

Initial buck converter setting
Buck converter reading after a few turns

Disconnect the power from the switch, remove your multimeter and alligator clips, screw together the buck converter case and put the top back on the CSS-326. You’re done!

My final results were that the switch ended up running about 30c cooler and the SFP was 11c cooler.

CSS-326-254G-2S+RM – 1 hour – CPU Temperature – Average 40c
CSS-326-254G-2S+RM – 1 hour – SFP+ Temperature – Average 32c

I get MikroTik saving cost by not including a fan in the switch but I really wish they would have at least installed a connector on the J2 to make adding a fan an easy option.

Update – 2022-08-20

I moved my entire homelab off my old Ubiquiti switch yesterday and have some real word temperatures with actual load on the switch:

The first low section on the left was the switch idling with no load while I configured VLANs and LAGs. The gap is me unplugging it, sliding it under my Ubiquiti switch and powering it back on. The initial high temperature (45.8c) was from the Ubiquiti smothering it while I did cable swaps. I eventually removed the Ubiquiti switch and the temperatures dropped a bit.

Parts List

Buck Converter – I used a “LM2596 DC-DC Buck Converter Step Down Module Power Supply DIP Output 1.25V-30V 3A”. There are a ton of these on Amazon. Here is a non-referral link to a 10pack I bought.

Noctua NF-A4x20 – Since there is plenty of room in the case I went with the 40mm * 20mm version of this fan to get the most air movement possible.

Buck Converter Case – I printed one of these to insulate the buck converter from the chassis of the switch.

Baffle – Completely optional but I designed and printed one of these to block off the section of the case to the right of the fan mount. Seemed pointless to circulate that air since there are no electronics in there except the buck converter.

Script for detecting potentially vulnerable Log4j jars [CVE-2021-44228] on Windows Server

Update 2021-12-18 – This looks like a much more competent script for detecting this vulnerability and there is a python version for Linux:

Updated 2021-12-17 – Script is v1.4 and looks for .war files now too

Original post below

Inspired by the one-liner here:

gci 'C:\' -rec -force -include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path

I wrote a script to expand on the command, support Windows Server 2008 onward and to be more automated.

This script is basically the one liner with a bit of logic to get all the local fixed disks on a server and iterate through them all looking for Log4j jar file:

    Checks the local system for Log4Shell Vulnerability [CVE-2021-44228]
    Gets a list of all volumes on the server, loops through searching each disk for Log4j stuff
    Using base search from

    Version History
        1.0 - Initial release
        1.1 - Changed ErrorAction to "Continue" instead of stopping the script
        1.2 - Went back to SilentlyContinue, so much noise
        1.3 - Borrowed some improvements from @cedric2bx (
                Replace attribute -Include by -Filter (prevent unauthorized access exception stopping scan)
                Remove duplicate path with Get-Unique cmdlet
        1.4 - Added .war support thanks to @djblazkowicz (
    Created by Eric Schewe 2021-12-13
    Modified by Cedric BARBOTIN 2021-12-14

# Get Windows Version string
$windowsVersion = (Get-WmiObject -class Win32_OperatingSystem).Caption

# Server 2008 (R2)
if ($windowsVersion -like "*2008*") {

    $disks = [System.IO.DriveInfo]::getdrives() | Where-Object {$_.DriveType -eq "Fixed"}

# Everything else
else {

    $disks = Get-Volume | Where-Object {$_.DriveType -eq "Fixed"}


# I have no idea why I had to write it this way and why .Count didn't just work
$diskCount = $disks | Measure-Object | Select-Object Count -ExpandProperty Count

Write-Host -ForegroundColor Green "$(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Starting the search of $($diskCount) disks"

foreach ($disk in $disks) {

    # One liner from
    # gci 'C:\' -rec -force -include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path

    # Server 2008 (R2)
    if ($windowsVersion -like "*2008*") {

        Write-Host -ForegroundColor Yellow "  $(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Checking $($disk.Name): - $($disk.VolumeLabel)"
        Get-ChildItem "$($disk.Name)" -Recurse -Force -Include @("*.jar","*.war") -ErrorAction SilentlyContinue | ForEach-Object { Select-String "JndiLookup.class" $_ } | Select-Object -ExpandProperty Path | Get-Unique

    # Everything else
    else {

        Write-Host -ForegroundColor Yellow "  $(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Checking $($disk.DriveLetter): - $($disk.VolumeLabel)"
        Get-ChildItem "$($disk.DriveLetter):\" -Recurse -Force -Include @("*.jar","*.war") -ErrorAction SilentlyContinue | ForEach-Object { Select-String "JndiLookup.class" $_ } | Select-Object -ExpandProperty Path | Get-Unique



Write-Host -ForegroundColor Green "$(Get-Date -Format "yyyy-MM-dd H:mm:ss") - Done checking all drives"

Sample output with nothing found:

check_CVE-2021-44228.ps1 sample output

Sample output with something found:

check_CVE-2021-44228.ps1 sample output 2

Good luck everyone.

Windows Defender Advanced Threat Protection Service will not start after November 2021 updates

Update – 2021-12-15 – I can confirm that the December Windows Updates have fixed this issue for us.


After installing OS updates on all of our servers in November 2021 we ended up with three servers, all running 2019 Core and all Domain Controllers, where the Windows Defender Advanced Threat Protection Service would not start.

With out the Windows Defender Advanced Threat Protection Service running these servers do not report to M365 ATP.

Manually trying to start the service results in an Error 1053:

Error 1053

and via PowerShell:

PS C:\Users\me> Start-Service sense
Start-Service : Service 'Windows Defender Advanced Threat Protection Service (sense)' cannot be started due to the
following error: Cannot start service sense on computer '.'.
At line:1 char:1
+ Start-Service sense
+ ~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (System.ServiceProcess.ServiceController:ServiceController) [Start-Service],
    + FullyQualifiedErrorId : CouldNotStartService,Microsoft.PowerShell.Commands.StartServiceCommand

Microsoft Support has confirmed with me this is a known issue with the November 2021 updates and should be addressed in December 2021 updates.

Hopefully this saves you a support ticket.