Private/Kinds/Security.ps1

# The Security Kind: what is scanning every file this machine touches.
#
# One antivirus product is how a machine is supposed to be. Two is a configuration nobody
# chose deliberately - a vendor product installed without Defender being stood down - and
# it is one of the few findings that explains a uniformly slow machine on its own, because
# every file open is scanned twice by two products that also contend with each other.

# Windows Security Center reports each product's state as a bitmask. The bit that matters
# is whether on-access scanning is running: a product installed but switched off is not
# scanning anything and must not count towards the total.
#
# A constant rather than a Check Definition parameter: it is a fact about the Windows API,
# and no Customer has an opinion about it.
$script:SecurityRealtimeScanningBit = 0x1000

function Get-SecurityData {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([hashtable]$Parameters = @{})

    $products = @()
    $available = $true
    try {
        $products = @(Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntiVirusProduct -ErrorAction Stop |
            ForEach-Object {
                # The raw bitmask, undecoded. Which products count is the Judge's call.
                [pscustomobject]@{
                    DisplayName  = "$($_.displayName)"
                    ProductState = $_.productState
                }
            })
    }
    catch {
        # Server SKUs and some hardened builds do not expose Security Center at all, which
        # is not the same fact as a machine with no antivirus on it.
        $available = $false
    }

    [pscustomobject]@{
        PSTypeName            = 'Gutcheck.Data.Security'
        SecurityCentrePresent = $available
        AntivirusProducts     = $products
    }
}

function ConvertTo-SecurityFinding {
    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()]$Data,
        [hashtable]$Parameters = @{}
    )

    New-AntivirusFinding -Data $Data -Parameters $Parameters
}

function ConvertTo-SecuritySection {
    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()]$Data)

    $products = Get-DataCollection $Data 'AntivirusProducts'
    if (-not $products.Count) { return }

    New-Section -Title (Get-Text 'Title.Security.AntivirusProductsRegisteredWith') -Row @(
        $products | ForEach-Object {
            [pscustomobject]@{
                Product      = $_.DisplayName
                ProductState = $_.ProductState
                Scanning     = (Test-AntivirusScanning -ProductState $_.ProductState)
            }
        }
    )
}

function Test-AntivirusScanning {
    <#
    .SYNOPSIS
        Whether a Security Center product state says on-access scanning is running.
    .DESCRIPTION
        Read from the bitmask rather than from the display name, because the display name
        is a vendor's marketing string and says nothing about whether the product is on.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param([AllowNull()]$ProductState)

    $state = ConvertTo-Number $ProductState
    if ($null -eq $state) { return $false }
    ([int]$state -band $script:SecurityRealtimeScanningBit) -ne 0
}

function New-AntivirusFinding {
    [CmdletBinding()]
    param([AllowNull()]$Data, [hashtable]$Parameters)

    # More than one scanner is the problem, so the threshold is a count of active products.
    $warnAbove = Get-Parameter $Parameters 'ActiveAntivirusWarnAbove' 1

    if (-not (Get-DataProperty $Data 'SecurityCentrePresent')) {
        return New-UnavailableFinding -Category Security -Check (Get-Text 'Check.Security.ActiveAntivirus') `
            -Hint (Get-Text 'Hint.Security.WindowsSecurityCenterDidNot')
    }

    $active = @((Get-DataCollection $Data 'AntivirusProducts') |
        Where-Object { Test-AntivirusScanning -ProductState $_.ProductState })

    if (-not $active.Count) {
        # The script said nothing here, which in a Report is indistinguishable from a
        # machine that was never asked. A Windows machine with nothing scanning it has
        # either had its protection turned off or had it fail.
        return New-Finding -Category Security -Check (Get-Text 'Check.Security.ActiveAntivirus') -Severity WARN -Value (Get-Text 'Value.Security.NoneActive') `
            -Hint (Get-Text 'Hint.Security.NothingIsScanningThisMachine')
    }

    New-Finding -Category Security -Check (Get-Text 'Check.Security.ActiveAntivirus') `
        -Severity (Get-Severity $active.Count $warnAbove ([double]::MaxValue)) `
        -Value (($active | ForEach-Object { $_.DisplayName }) -join ', ') `
        -Hint (Get-Text 'Hint.Security.MoreThanOneActiveAV')
}