#Requires -Version 5.1 <# .SYNOPSIS Verify Smart App Control is off. .DESCRIPTION Passes only when both of these are true: - HKLM\...\CI\Policy\VerifiedAndReputablePolicyState is 0 - CiTool shows VerifiedAndReputableDesktop and VerifiedAndReputableDesktopEvaluation as not currently enforced MinValueSeen and SacLearningModeSwitch are reported, not required. Those two are stickiness values. A missing or unchanged value is not a failure. Exit code 0 = Off. Exit code 1 = not Off, or the check could not be completed. Run this after the reboot, from an elevated window on the normal desktop. Safe Mode is not a pass. This script does not read Safe Mode, and it does not change policy. #> Set-ExecutionPolicy -Scope Process Bypass -Force $ErrorActionPreference = 'Stop' function Get-DwordOrNull { param([string]$Path, [string]$Name) if (-not (Test-Path -LiteralPath $Path)) { return $null } $item = Get-ItemProperty -LiteralPath $Path -Name $Name -ErrorAction SilentlyContinue if ($null -eq $item) { return $null } return [int]$item.$Name } function Test-CiEnforced { param($Value) if ($null -eq $Value) { return 'unread' } $text = ([string]$Value).Trim() if ($text -match '^(?i)true$') { return 'true' } if ($text -match '^(?i)false$') { return 'false' } return 'unread' } function Add-CiPolicy { param($List, $Current) if ($null -eq $Current) { return } if ($Current.Count -eq 0) { return } $List.Add([pscustomobject]$Current) | Out-Null } function ConvertFrom-CiToolText { param([string]$Raw) $list = New-Object System.Collections.Generic.List[object] $current = $null foreach ($line in ($Raw -split "`r?`n")) { if ($line -match '^\s*Policy:\s*$') { Add-CiPolicy -List $list -Current $current $current = [ordered]@{} continue } if ($line -match '^\s*Policy ID:\s*(.*)$') { Add-CiPolicy -List $list -Current $current $current = [ordered]@{} $current['Policy ID'] = $Matches[1].Trim() continue } if ($null -ne $current -and $line -match '^\s*([^:]+):\s*(.*)$') { $current[$Matches[1].Trim()] = $Matches[2].Trim() } } Add-CiPolicy -List $list -Current $current $parsed = $true if ($list.Count -eq 0 -and $Raw -match '(?m)^\s*Policy ID:') { $parsed = $false } [pscustomobject]@{ Policies = @($list.ToArray()) Raw = $Raw Parsed = $parsed } } function ConvertFrom-CiToolJson { param([string]$Raw) try { $parsedJson = $Raw | ConvertFrom-Json } catch { return [pscustomobject]@{ Policies = @(); Raw = $Raw; Parsed = $false } } if ($null -eq $parsedJson -or $null -eq $parsedJson.PSObject.Properties['Policies']) { return [pscustomobject]@{ Policies = @(); Raw = $Raw; Parsed = $false } } $list = New-Object System.Collections.Generic.List[object] foreach ($item in @($parsedJson.Policies)) { if ($null -eq $item) { continue } $name = $item.FriendlyName if ([string]::IsNullOrWhiteSpace([string]$name)) { $name = $item.'Friendly Name' } $enforced = $item.IsEnforced if ($null -eq $enforced) { $enforced = $item.IsCurrentlyEnforced } $authorized = $item.IsAuthorized $enforcedText = $null if ($null -ne $enforced) { $enforcedText = [string]$enforced } $authorizedText = $null if ($null -ne $authorized) { $authorizedText = [string]$authorized } $list.Add([pscustomobject]@{ 'Friendly Name' = [string]$name 'Is Currently Enforced' = $enforcedText 'Is Authorized' = $authorizedText }) | Out-Null } [pscustomobject]@{ Policies = @($list.ToArray()) Raw = $Raw Parsed = $true } } function Test-CiRawNamesPolicy { param([string]$Raw, [string]$Name) if ([string]::IsNullOrWhiteSpace($Raw)) { return $false } $escaped = [regex]::Escape($Name) if ($Raw -match ("(?m)^\s*Friendly Name:\s*{0}\s*$" -f $escaped)) { return $true } if ($Raw -match ('"FriendlyName"\s*:\s*"' + $escaped + '"')) { return $true } return $false } function Format-SacValue { param($Value) if ($null -eq $Value) { return '' } return [string]$Value } function Get-SacOffReport { param($State, $MinSeen, $Learning, $PolicyResult) $lines = New-Object System.Collections.Generic.List[string] $failed = $false [void]$lines.Add(("VerifiedAndReputablePolicyState = {0}" -f (Format-SacValue $State))) [void]$lines.Add(("VerifiedAndReputablePolicyStateMinValueSeen = {0}" -f (Format-SacValue $MinSeen))) [void]$lines.Add(("SacLearningModeSwitch = {0}" -f (Format-SacValue $Learning))) if ($State -ne 0) { [void]$lines.Add('FAIL Policy state is not 0 (Off).') $failed = $true } else { [void]$lines.Add('OK Policy state is 0 (Off).') } $watched = @( 'VerifiedAndReputableDesktop', 'VerifiedAndReputableDesktopEvaluation' ) if ($null -eq $PolicyResult -or -not $PolicyResult.Parsed) { [void]$lines.Add('FAIL CiTool output could not be read.') $failed = $true } else { foreach ($name in $watched) { $policy = @($PolicyResult.Policies) | Where-Object { $_.'Friendly Name' -eq $name } | Select-Object -First 1 if (-not $policy) { if (Test-CiRawNamesPolicy -Raw ([string]$PolicyResult.Raw) -Name $name) { [void]$lines.Add("FAIL $name appeared in CiTool output but was not read.") $failed = $true } else { [void]$lines.Add("OK $name is not present.") } continue } $enforced = $policy.'Is Currently Enforced' $authorized = $policy.'Is Authorized' [void]$lines.Add(( " {0}: Is Currently Enforced = {1}, Is Authorized = {2}" -f $name, $enforced, $authorized )) $flag = Test-CiEnforced -Value $enforced if ($flag -eq 'true') { [void]$lines.Add("FAIL $name is currently enforced.") $failed = $true } elseif ($flag -eq 'false') { [void]$lines.Add("OK $name is not enforced.") } else { [void]$lines.Add("FAIL $name enforced state was not read.") $failed = $true } } } [void]$lines.Add('') if ($failed) { [void]$lines.Add('Smart App Control is NOT off.') $code = 1 } else { [void]$lines.Add('Smart App Control is off.') $code = 0 } [pscustomobject]@{ Lines = $lines.ToArray() Code = $code } } function Get-CiPolicies { $citool = Join-Path $env:SystemRoot 'System32\CiTool.exe' if (-not (Test-Path -LiteralPath $citool)) { throw "CiTool.exe not found at $citool" } $jsonRaw = & $citool -lp -json 2>&1 | Out-String if ($LASTEXITCODE -eq 0) { $fromJson = ConvertFrom-CiToolJson -Raw $jsonRaw if ($fromJson.Parsed) { return $fromJson } } $raw = & $citool -lp 2>&1 | Out-String if ($LASTEXITCODE -ne 0) { throw "CiTool.exe -lp exited $LASTEXITCODE" } return (ConvertFrom-CiToolText -Raw $raw) } # File run performs the check. Dot-sourcing only loads the functions. if ($MyInvocation.InvocationName -ne '.') { $policyKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' $protectedKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Protected' $defenderKey = 'HKLM:\SOFTWARE\Microsoft\Windows Defender' $state = Get-DwordOrNull -Path $policyKey -Name 'VerifiedAndReputablePolicyState' $minSeen = Get-DwordOrNull -Path $protectedKey -Name 'VerifiedAndReputablePolicyStateMinValueSeen' $learning = Get-DwordOrNull -Path $defenderKey -Name 'SacLearningModeSwitch' try { $policies = Get-CiPolicies } catch { $policies = [pscustomobject]@{ Policies = @() Raw = $_.Exception.Message Parsed = $false } } $report = Get-SacOffReport -State $state -MinSeen $minSeen -Learning $learning -PolicyResult $policies if (-not $policies.Parsed -and -not [string]::IsNullOrWhiteSpace([string]$policies.Raw)) { Write-Host ("FAIL {0}" -f $policies.Raw.Trim()) } foreach ($line in @($report.Lines)) { Write-Host $line } exit $report.Code }