Public/Invoke-DefenderDcUsbTest.ps1

function Invoke-DefenderDcUsbTest {
<#
.SYNOPSIS
    End-to-end USB stick test driver for the Defender Device Control policy.
 
.DESCRIPTION
    Operator-facing test that brackets a manual USB stick write attempt with
    DC policy apply and rollback. Captures a transcript and reads back the
    last 2 minutes of Defender 1109/1110/1111 events (softened to
    informational on modern MDE builds where events route to Defender XDR
    Advanced Hunting only).
 
    Phases:
      1. Pre-flight: Defender and Sense state.
      2. Capture pre-state of DC policy.
      3. Apply DC at -StartMode.
      4. Verify static state.
      5. Operator interactive: replug stick, attempt write, observe result.
      6. Read event-log signal (best-effort; zero events is not a failure).
      7. Restore DC to pre-test state (unless -KeepDcApplied).
 
    Requires administrator elevation.
 
.PARAMETER Drive
    Drive letter of the USB stick to test, e.g. 'E' or 'E:'. A trailing
    colon is accepted and stripped automatically.
 
.PARAMETER StartMode
    DC mode to apply during the test. Default 'Audit' (safer first proof).
    'Enforce' blocks writes and fires deny events.
 
.PARAMETER KeepDcApplied
    Switch. If set, skip the final rollback and leave DC at -StartMode after
    the test completes. Default: rollback to whatever DC mode was active
    before the test began.
 
.EXAMPLE
    Invoke-DefenderDcUsbTest -Drive E:
 
    Run an Audit-mode USB test against E: drive.
 
.EXAMPLE
    Invoke-DefenderDcUsbTest -Drive E: -StartMode Enforce -KeepDcApplied
 
    Run an Enforce-mode test and leave Enforce active afterwards.
 
.OUTPUTS
    PSCustomObject with properties:
      Failures - [int] count of failed checks across pre-flight and verification
      TranscriptPath - absolute path to the per-invocation transcript log
      StartMode - the -StartMode value the test applied ('Audit' or 'Enforce')
      EventsCaptured - [int] Defender events 1109/1110/1111 captured in the test window (informational; modern MDE routes to XDR)
      PreTestMode - DC mode read before -StartMode was applied (used for rollback)
 
.LINK
    https://lukeevanstech.github.io/defender-device-control-unmanaged/howto/run-end-to-end-test/
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType('DefenderDeviceControlUnmanaged.UsbTestResult')]
    param(
        [Parameter(Mandatory)]
        [ValidatePattern('^[A-Za-z]:?$')]
        [string] $Drive,

        [Alias('Mode')]
        [ValidateSet('Audit','Enforce')]
        [string] $StartMode = 'Audit',

        [switch] $KeepDcApplied
    )

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

    if (-not (Test-DcIsElevated)) {
        throw "Invoke-DefenderDcUsbTest: must be run from an elevated PowerShell."
    }

    # Normalise drive letter: 'E:' -> 'E', 'E' -> 'E'
    $driveLetter = $Drive.TrimEnd(':').ToUpper()
    $drivePath   = "$driveLetter`:"

    $transcript = Start-DcTranscript -CmdletName 'Invoke-DefenderDcUsbTest'

    $failures = 0
    $failureRef = [ref]$failures

    $preMode       = $null
    $eventsCaptured = 0

    try {
        Write-Host ""
        Write-Host "================================================================" -ForegroundColor Cyan
        Write-Host " Invoke-DefenderDcUsbTest -- $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan
        Write-Host " Drive: $drivePath StartMode: $StartMode KeepDcApplied: $KeepDcApplied" -ForegroundColor Cyan
        Write-Host " Transcript: $transcript" -ForegroundColor Cyan
        Write-Host "================================================================" -ForegroundColor Cyan

        # --- Phase 1: Pre-flight ---
        Write-Host ""
        Write-Host "[1/7] Pre-flight" -ForegroundColor Cyan
        Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
        $defender = Get-DcComputerStatus
        Write-DcCheckResult 'Defender AM service enabled' { $defender.AMServiceEnabled } 'AMServiceEnabled must be True' $failureRef
        $sense = Get-Service -Name Sense -ErrorAction SilentlyContinue
        Write-DcCheckResult 'Sense service Running' { $sense -and $sense.Status -eq 'Running' } 'Sense must be Running' $failureRef

        # --- Phase 2: Capture pre-state of DC ---
        Write-Host ""
        Write-Host "[2/7] Capture pre-state of DC" -ForegroundColor Cyan
        Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
        $preState = Get-DefenderDcPolicy
        $preMode  = $preState.Mode
        Write-Host " Pre-test DC mode: $preMode" -ForegroundColor DarkGray

        if ($PSCmdlet.ShouldProcess($drivePath, "Run USB Device Control test in $StartMode mode")) {
            # --- Phase 3: Apply DC at StartMode ---
            Write-Host ""
            Write-Host "[3/7] Apply DC -Mode $StartMode" -ForegroundColor Cyan
            Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
            Set-DefenderDcPolicy -Mode $StartMode

            # --- Phase 4: Verify static state ---
            Write-Host ""
            Write-Host "[4/7] Verify static state" -ForegroundColor Cyan
            Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
            $verifyResult = Test-DefenderDcPolicy -ExpectMode $StartMode
            Write-DcCheckResult "Static verification passes for $StartMode" { $verifyResult } "Test-DefenderDcPolicy returned false" $failureRef

            # --- Phase 5: Operator interactive ---
            Write-Host ""
            Write-Host "[5/7] Operator interactive test" -ForegroundColor Cyan
            Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
            Write-Host " Action required:" -ForegroundColor Yellow
            Write-Host " 1. Unplug the USB stick from $drivePath (if currently mounted)." -ForegroundColor Yellow
            Write-Host " 2. Plug it back in. Wait for the drive letter to appear." -ForegroundColor Yellow
            Write-Host " 3. Open a file from $drivePath (read should always work)." -ForegroundColor Yellow
            Write-Host " 4. Try to copy a file TO $drivePath." -ForegroundColor Yellow
            Write-Host " Audit: succeeds, telemetry recorded in Defender XDR." -ForegroundColor Yellow
            Write-Host " Enforce: fails with 'The media is write protected'." -ForegroundColor Yellow
            Write-Host ""
            $startTime = Get-Date
            Read-Host " Press ENTER when finished with the manual test"

            # --- Phase 6: Read event-log signal (best-effort) ---
            Write-Host ""
            Write-Host "[6/7] Defender event-log lookup (last 2 minutes)" -ForegroundColor Cyan
            Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
            try {
                $events = @(Get-WinEvent -LogName 'Microsoft-Windows-Windows Defender/Operational' -MaxEvents 100 -ErrorAction SilentlyContinue |
                    Where-Object { $_.Id -in @(1109, 1110, 1111) } |
                    Where-Object { $_.TimeCreated -gt $startTime.AddMinutes(-2) })
                $eventsCaptured = $events.Count
                Write-Host " Events 1109/1110/1111 captured: $eventsCaptured" -ForegroundColor DarkGray
                if ($eventsCaptured -eq 0) {
                    Write-Host " (Modern MDE builds route DC events to Defender XDR Advanced Hunting only;" -ForegroundColor Yellow
                    Write-Host " zero local events is informational, not a failure. Check security.microsoft.com" -ForegroundColor Yellow
                    Write-Host " DeviceEvents | where ActionType == 'RemovableStoragePolicyTriggered' for evidence.)" -ForegroundColor Yellow
                } else {
                    $events | ForEach-Object { Write-Host " $($_.TimeCreated) Id=$($_.Id)" -ForegroundColor DarkGray }
                }
            } catch {
                Write-Host " (Event log lookup failed: $($_.Exception.Message))" -ForegroundColor Yellow
            }

            # --- Phase 7: Restore (unless KeepDcApplied) ---
            Write-Host ""
            Write-Host "[7/7] Restore pre-test DC state" -ForegroundColor Cyan
            Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray
            if ($KeepDcApplied) {
                Write-Host " -KeepDcApplied set; leaving DC at $StartMode." -ForegroundColor Yellow
            } else {
                if ($null -eq $preMode -or $preMode -eq $StartMode) {
                    Write-Host " Pre-test mode equals StartMode ($preMode); no rollback needed." -ForegroundColor DarkGray
                } else {
                    Write-Host " Rolling back to $preMode." -ForegroundColor DarkGray
                    Set-DefenderDcPolicy -Mode $preMode
                }
            }
        } else {
            Write-Host " Preview only; skipping policy apply, manual prompt, event lookup, and rollback." -ForegroundColor Yellow
        }

        Write-Host ""
        Write-Host "================================================================" -ForegroundColor Cyan
        if ($failureRef.Value -eq 0) {
            Write-Host " Invoke-DefenderDcUsbTest: ALL CHECKS PASSED" -ForegroundColor Green
        } else {
            Write-Host " Invoke-DefenderDcUsbTest: $($failureRef.Value) CHECKS FAILED" -ForegroundColor Red
        }
        Write-Host " Transcript: $transcript" -ForegroundColor Cyan
        Write-Host "================================================================" -ForegroundColor Cyan
    }
    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 { }
    }

    [pscustomobject]@{
        PSTypeName     = 'DefenderDeviceControlUnmanaged.UsbTestResult'
        Failures       = $failureRef.Value
        TranscriptPath = $transcript
        StartMode      = $StartMode
        EventsCaptured = $eventsCaptured
        PreTestMode    = $preMode
    }
}