Public/Invoke-DefenderDcOnboarding.ps1
|
function Invoke-DefenderDcOnboarding { <# .SYNOPSIS Onboard this Windows host to Microsoft Defender for Endpoint using the per-tenant local-script deployment method. .DESCRIPTION Wraps Microsoft's per-tenant onboarding .cmd with pre-flight checks, auto-detection of the onboarding ZIP in $env:USERPROFILE\Downloads\, extraction, elevated execution, and post-flight verification. Pre-flight: Defender AM service enabled, Sense service present, not already onboarded. Post-flight: Sense Running + Automatic, OnboardingState=1, OrgId populated. Requires administrator elevation. Captures a transcript under $env:LOCALAPPDATA\DefenderDeviceControlUnmanaged\. .PARAMETER OnboardingScript Optional path to either the per-tenant ZIP (e.g. WindowsDefenderATPOnboardingPackage.zip) or the extracted WindowsDefenderATP*OnboardingScript.cmd. If not supplied, searches $env:USERPROFILE\Downloads\ for files matching '*WindowsDefenderATPOnboarding*.zip' and uses the newest match. .PARAMETER PostFlightWaitSeconds Seconds to wait between the onboarding script exit and the post-flight checks. Defender service start and registry markers are async. Default 30. .EXAMPLE Invoke-DefenderDcOnboarding Auto-detect the onboarding ZIP in Downloads and run it. .EXAMPLE Invoke-DefenderDcOnboarding -OnboardingScript C:\onboard\WindowsDefenderATPOnboardingPackage.zip Explicitly pass the ZIP path. .OUTPUTS PSCustomObject with properties: SenseStatus - 'Running' / 'Stopped' / etc. from Get-Service -Name Sense, or $null SenseStartType - 'Automatic' / 'Manual' / etc., or $null OnboardingState - 1 when onboarded, 0/null otherwise (HKLM WATP Status) OrgId - tenant OrgId GUID string, or $null Failures - [int] count of failed pre-flight and post-flight checks TranscriptPath - absolute path to the per-invocation transcript log .LINK https://lukeevanstech.github.io/defender-device-control-unmanaged/howto/onboard-to-mde/ #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType('DefenderDeviceControlUnmanaged.OnboardingResult')] param( [string] $OnboardingScript, [Alias('SkipPostFlightWait')] [int] $PostFlightWaitSeconds = 30 ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Continue' if (-not (Test-DcIsElevated)) { throw "Invoke-DefenderDcOnboarding: must be run from an elevated PowerShell." } $ts = Get-Date -Format 'yyyyMMdd-HHmmss' $transcript = Start-DcTranscript -CmdletName 'Invoke-DefenderDcOnboarding' $failures = 0 $failureRef = [ref]$failures $senseAfter = $null $obsAfter = $null $orgId = $null $extractedTemp = $null try { Write-Host "" Write-Host "================================================================" -ForegroundColor Cyan Write-Host " Invoke-DefenderDcOnboarding -- $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" -ForegroundColor Cyan Write-Host " Transcript: $transcript" -ForegroundColor Cyan Write-Host "================================================================" -ForegroundColor Cyan Write-Host "" Write-Host "[1/4] Pre-flight checks" -ForegroundColor Cyan Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray $defender = Get-DcComputerStatus Write-Host " Defender AM engine: $($defender.AMEngineVersion)" -ForegroundColor DarkGray Write-Host " Defender AM product: $($defender.AMProductVersion)" -ForegroundColor DarkGray Write-Host " Tamper Protection: $($defender.IsTamperProtected)" -ForegroundColor DarkGray Write-DcCheckResult 'Defender AM service enabled' { $defender.AMServiceEnabled } 'AMServiceEnabled must be True' $failureRef $sense = Get-Service -Name Sense -ErrorAction SilentlyContinue Write-DcCheckResult 'Sense service is installed' { $null -ne $sense } 'Sense service must exist on the box' $failureRef $watpStatus = 'HKLM:\SOFTWARE\Microsoft\Windows Advanced Threat Protection\Status' $currentOnboardingState = $null if (Test-Path -LiteralPath $watpStatus) { $obs = (Get-ItemProperty -LiteralPath $watpStatus -Name OnboardingState -ErrorAction SilentlyContinue).OnboardingState if ($null -ne $obs) { $currentOnboardingState = [int]$obs } } Write-Host " Current OnboardingState: $currentOnboardingState" -ForegroundColor DarkGray $alreadyOnboarded = ($sense -and $sense.Status -eq 'Running' -and $currentOnboardingState -eq 1) if ($alreadyOnboarded) { throw "Box is already onboarded (Sense=Running, OnboardingState=1). Do not re-run onboarding; if you need to re-onboard, offboard first." } Write-Host "" Write-Host "[2/4] Locate onboarding script" -ForegroundColor Cyan Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray $cmdPath = $null $zipPath = $null if ($OnboardingScript) { if (-not (Test-Path -LiteralPath $OnboardingScript)) { throw "OnboardingScript path does not exist: $OnboardingScript" } if ($OnboardingScript.ToLower().EndsWith('.cmd')) { $cmdPath = (Resolve-Path -LiteralPath $OnboardingScript).Path } elseif ($OnboardingScript.ToLower().EndsWith('.zip')) { $zipPath = $OnboardingScript } else { throw "OnboardingScript must be a .zip or a .cmd file: $OnboardingScript" } } else { $downloads = Join-Path $env:USERPROFILE 'Downloads' $zips = @(Get-ChildItem -LiteralPath $downloads -Filter '*WindowsDefenderATPOnboarding*.zip' -ErrorAction SilentlyContinue) if ($zips.Count -eq 0) { throw "No onboarding ZIP found in $downloads matching *WindowsDefenderATPOnboarding*.zip. Download from security.microsoft.com (Settings -> Endpoints -> Onboarding -> Local Script) or pass -OnboardingScript explicitly." } $newest = $zips | Sort-Object LastWriteTime -Descending | Select-Object -First 1 Write-Host " Found ZIP: $($newest.FullName) ($($newest.LastWriteTime))" -ForegroundColor DarkGray $zipPath = $newest.FullName } $shouldRunOnboarding = $PSCmdlet.ShouldProcess( $(if ($cmdPath) { $cmdPath } else { $zipPath }), 'Run MDE onboarding script (cmd.exe)') if ($shouldRunOnboarding) { if ($zipPath) { $extractedTemp = Join-Path $env:TEMP "mde-onboard-$ts" New-Item -ItemType Directory -Path $extractedTemp | Out-Null Expand-Archive -LiteralPath $zipPath -DestinationPath $extractedTemp -Force $found = Get-ChildItem -LiteralPath $extractedTemp -Filter 'WindowsDefenderATP*OnboardingScript.cmd' -Recurse | Select-Object -First 1 if (-not $found) { throw "Extracted ZIP did not contain WindowsDefenderATP*OnboardingScript.cmd" } $cmdPath = $found.FullName } Write-Host " Using script: $cmdPath" -ForegroundColor DarkGray Write-Host "" Write-Host "[3/4] Run onboarding script (elevated)" -ForegroundColor Cyan Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray $scriptExit = 0 & cmd.exe /c "`"$cmdPath`"" $scriptExit = $LASTEXITCODE Write-Host " Onboarding script exit code: $scriptExit" -ForegroundColor DarkGray if ($scriptExit -ne 0) { Write-Host " Onboarding script returned non-zero. Proceeding to post-flight to capture actual state." -ForegroundColor Yellow } Write-Host " Settling $PostFlightWaitSeconds seconds before post-flight checks..." -ForegroundColor DarkGray Start-Sleep -Seconds $PostFlightWaitSeconds Write-Host "" Write-Host "[4/4] Post-flight verification" -ForegroundColor Cyan Write-Host "----------------------------------------------------------------" -ForegroundColor DarkGray $senseAfter = Get-Service -Name Sense -ErrorAction SilentlyContinue Write-DcCheckResult 'Sense.Status = Running' { $senseAfter -and $senseAfter.Status -eq 'Running' } 'Sense service did not reach Running state' $failureRef Write-DcCheckResult 'Sense.StartType = Automatic' { $senseAfter -and $senseAfter.StartType -eq 'Automatic' } 'Sense start type should be Automatic post-onboarding' $failureRef if (Test-Path -LiteralPath $watpStatus) { $obs = (Get-ItemProperty -LiteralPath $watpStatus -Name OnboardingState -ErrorAction SilentlyContinue).OnboardingState if ($null -ne $obs) { $obsAfter = [int]$obs } } Write-DcCheckResult 'OnboardingState = 1' { $obsAfter -eq 1 } "expected 1, got '$obsAfter'" $failureRef if (Test-Path -LiteralPath $watpStatus) { $orgId = (Get-ItemProperty -LiteralPath $watpStatus -Name OrgId -ErrorAction SilentlyContinue).OrgId } Write-DcCheckResult 'OrgId is populated' { -not [string]::IsNullOrEmpty($orgId) } 'OrgId must be a non-empty string' $failureRef Write-Host "" Write-Host "Final state:" -ForegroundColor Yellow Write-Host " Sense.Status : $(if ($senseAfter) { $senseAfter.Status } else { '(missing)' })" -ForegroundColor DarkGray Write-Host " Sense.StartType : $(if ($senseAfter) { $senseAfter.StartType } else { '(missing)' })" -ForegroundColor DarkGray Write-Host " OnboardingState : $obsAfter" -ForegroundColor DarkGray Write-Host " OrgId : $orgId" -ForegroundColor DarkGray Write-Host "" Write-Host "================================================================" -ForegroundColor Cyan if ($failureRef.Value -eq 0) { Write-Host " Invoke-DefenderDcOnboarding: ALL CHECKS PASSED" -ForegroundColor Green } else { Write-Host " Invoke-DefenderDcOnboarding: $($failureRef.Value) CHECKS FAILED" -ForegroundColor Red } Write-Host " Transcript: $transcript" -ForegroundColor Cyan Write-Host "================================================================" -ForegroundColor Cyan } else { Write-Host " Preview only; skipping ZIP extraction, script execution, wait, and post-flight checks." -ForegroundColor Yellow } } finally { if ($extractedTemp -and (Test-Path -LiteralPath $extractedTemp)) { Remove-Item -LiteralPath $extractedTemp -Recurse -Force -ErrorAction SilentlyContinue } # 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.OnboardingResult' SenseStatus = if ($senseAfter) { $senseAfter.Status } else { $null } SenseStartType = if ($senseAfter) { $senseAfter.StartType } else { $null } OnboardingState = $obsAfter OrgId = $orgId Failures = $failureRef.Value TranscriptPath = $transcript } } |