Public/Set-DefenderDcPolicy.ps1

function Set-DefenderDcPolicy {
<#
.SYNOPSIS
    Apply or remove the Defender Device Control read-only USB policy on an
    unmanaged Windows 11 device, using the canonical Local GPO registry
    surface (file-path REG_SZ + XML, per WindowsDefender.admx).
 
.DESCRIPTION
    Writes 5 registry values under HKLM\SOFTWARE\Policies\Microsoft\Windows
    Defender\... that point Defender at policy XMLs describing the device
    groups and per-class deny rules. By default, ships starter XMLs covering
    removable storage, WPD/MTP, and optical drives. Supply -GroupsXmlPath
    and -RulesXmlPath to deploy your own.
 
    Requires administrator elevation. Requires MDE attach for the engine to
    consume policy (registry writes succeed on any box; engine activation
    requires Microsoft Defender for Endpoint).
 
.PARAMETER Mode
    Audit (log without block), Enforce (block + log), or Off (remove policy).
 
.PARAMETER GroupsXmlPath
    Optional absolute path to a PolicyGroups.xml. Defaults to the shipped
    starter XML.
 
.PARAMETER RulesXmlPath
    Optional absolute path to a PolicyRules.<Mode>.xml. Defaults to the
    shipped starter XML matching the selected -Mode.
 
.PARAMETER SkipMpCmdRunValidation
    Skip the engine-side XML preflight via MpCmdRun.exe -DeviceControl
    -TestPolicyXml. Off by default - validation recommended.
 
.PARAMETER SkipGpUpdate
    Skip the trailing gpupdate /force. gpupdate is the canonical trigger
    that makes Defender consume the policy. Skipping leaves the registry
    written but the engine may not pick up the policy until the next
    OS-driven refresh.
 
.EXAMPLE
    Set-DefenderDcPolicy -Mode Audit -WhatIf
 
    Preview the planned registry writes without applying.
 
.EXAMPLE
    Set-DefenderDcPolicy -Mode Enforce
 
    Apply policy in Enforce mode.
 
.EXAMPLE
    Set-DefenderDcPolicy -Mode Enforce -GroupsXmlPath 'C:\MyPolicy\Groups.xml' -RulesXmlPath 'C:\MyPolicy\Rules.xml'
 
    Deploy a custom policy XML pair.
 
.EXAMPLE
    Set-DefenderDcPolicy -Mode Off
 
    Remove the policy entirely.
 
.LINK
    https://lukeevanstech.github.io/defender-device-control-unmanaged/
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('Audit','Enforce','Off')]
        [string] $Mode,

        [string] $GroupsXmlPath,

        [string] $RulesXmlPath,

        [switch] $SkipMpCmdRunValidation,

        [switch] $SkipGpUpdate
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if (-not (Test-DcIsElevated)) { throw "Set-DefenderDcPolicy: must be run elevated." }

    $transcript = Start-DcTranscript -CmdletName 'Set-DefenderDcPolicy'
    try {
        try { $defender = Get-DcComputerStatus } catch {
            # Wrap the original ErrorRecord so the caller still has the typed exception + stack trace.
            throw [System.Management.Automation.ErrorRecord]::new(
                [System.InvalidOperationException]::new(
                    "Set-DefenderDcPolicy: Get-MpComputerStatus failed: $($_.Exception.Message)",
                    $_.Exception),
                'DefenderDeviceControlUnmanaged.DefenderQueryFailed',
                [System.Management.Automation.ErrorCategory]::ResourceUnavailable,
                $null)
        }
        if (-not $defender.AMServiceEnabled) { throw "Set-DefenderDcPolicy: Defender service not enabled." }
        Write-Verbose "Defender AM engine $($defender.AMEngineVersion), TamperProtection=$($defender.IsTamperProtected)"

        if ($Mode -eq 'Off') {
            Write-Verbose "Removing Device Control policy."
            if ($PSCmdlet.ShouldProcess('Device Control policy registry', 'Remove policy state')) {
                Remove-DcPolicy
                if (-not $SkipGpUpdate -and $PSCmdlet.ShouldProcess('Group Policy', 'gpupdate /force')) {
                    & gpupdate.exe /force 2>&1 | ForEach-Object { Write-Verbose " $_" }
                }
                if ($PSCmdlet.ShouldProcess('Defender engine', 'Update-MpSignature')) {
                    Update-MpSignature -UpdateSource MMPC -ErrorAction SilentlyContinue
                }
            }
            return
        }

        $defaultPolicyDir = Join-Path $PSScriptRoot '..\policy'
        if (-not $GroupsXmlPath) { $GroupsXmlPath = Join-Path $defaultPolicyDir 'PolicyGroups.xml' }
        if (-not $RulesXmlPath)  { $RulesXmlPath  = Join-Path $defaultPolicyDir "PolicyRules.$Mode.xml" }
        $GroupsXmlPath = [System.IO.Path]::GetFullPath($GroupsXmlPath)
        $RulesXmlPath  = [System.IO.Path]::GetFullPath($RulesXmlPath)

        foreach ($p in $GroupsXmlPath, $RulesXmlPath) {
            if (-not (Test-Path -LiteralPath $p -PathType Leaf)) {
                throw "Set-DefenderDcPolicy: required policy file not found: $p"
            }
        }

        Write-Verbose "Groups: $GroupsXmlPath"
        Write-Verbose "Rules: $RulesXmlPath"

        # Use the public Test-DefenderDcPolicyXml validator (which layers BOM /
        # xml-declaration / PolicyRule.Name-as-child / Options-bitmask checks on
        # top of Read-DcPolicyXml + MpCmdRun) so a caller-supplied XML faces the
        # same contract whether they ran Test-DefenderDcPolicyXml first or not.
        if (-not (Test-DefenderDcPolicyXml -Path $GroupsXmlPath -Kind Groups -SkipEngineValidation:$SkipMpCmdRunValidation)) {
            throw "Set-DefenderDcPolicy: GroupsXmlPath failed validation: $GroupsXmlPath"
        }
        if (-not (Test-DefenderDcPolicyXml -Path $RulesXmlPath -Kind Rules -SkipEngineValidation:$SkipMpCmdRunValidation)) {
            throw "Set-DefenderDcPolicy: RulesXmlPath failed validation: $RulesXmlPath"
        }

        $manifest = Get-DcRegistryManifest -GroupsXmlPath $GroupsXmlPath -RulesXmlPath $RulesXmlPath

        if ($PSCmdlet.ShouldProcess('Device Control policy registry', "Apply $Mode policy state")) {
            Write-Verbose "Removing any prior Device Control policy state before re-applying."
            Remove-DcPolicy

            Invoke-DcRegistryWrites -Manifest $manifest

            if (-not $SkipGpUpdate -and $PSCmdlet.ShouldProcess('Group Policy', 'gpupdate /force')) {
                & gpupdate.exe /force 2>&1 | ForEach-Object { Write-Verbose " $_" }
            }

            if ($PSCmdlet.ShouldProcess('Defender engine', 'Update-MpSignature')) {
                Update-MpSignature -UpdateSource MMPC -ErrorAction SilentlyContinue
            }
        }
    }
    finally {
        # Stop-Transcript throws "host is not currently transcribing" under -WhatIf
        # (Start-Transcript honors $WhatIfPreference and becomes a no-op). The
        # finally block must clean up regardless; swallow the benign case.
        try { Stop-Transcript | Out-Null } catch { }
        Write-Verbose "Set-DefenderDcPolicy transcript: $transcript"
    }
}