public/xspm/Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices.ps1

<#
.SYNOPSIS
    Test to find devices that have critical credentials stored on devices that are not protected by TPM.

.DESCRIPTION
    Test to find devices that have critical credentials stored on devices that are not protected by TPM.

.OUTPUTS
    [bool] - Returns $true if no devices are found, $false if any are found, $null if skipped or prerequisites not met.

.EXAMPLE
    Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices

.LINK
    https://maester.dev/docs/commands/Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices
#>


function Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This test checks for devices that have critical credentials stored on devices that are not protected by TPM.')]
    [OutputType([bool])]
    param()

    Write-Verbose "Get raw data from Exposure Management..."
    $Query = @"
        let no_tpm_devices = (
        ExposureGraphNodes
        // Get device nodes with their inventory ID
        | mv-expand EntityIds
        | where EntityIds.type == "DeviceInventoryId"
        // Get interesting properties
        | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
            TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
            TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
            TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
            DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
            DeviceId = tostring(EntityIds.id)
        | extend DeviceName = iff(isempty(DeviceName), NodeName, DeviceName)
        // Search for distinct devices
        | distinct NodeId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
        // Get device with no TPM enabled
        | where TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true"
        | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
            TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
            TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
    );
    let critical_users = toscalar(
        // Search for critical users
        ExposureGraphNodes
        | where NodeLabel == "user"
        | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
        | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
        | where CriticalityLevel == 0
        | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
        | summarize make_set(NodeName)
    );
    // Make graph for max of 3 edges, where we start from a device and end with an user
    ExposureGraphEdges
    | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
    | graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
        where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user" and TargetNode.NodeName in ( critical_users )
        project SourceNodeName = SourceNode.NodeName,
        SourceNodeId = SourceNode.NodeId,
        Edges = anyEdge.EdgeLabel,
        TargetNodeId = TargetNode.NodeId,
        TargetNodeName = TargetNode.NodeName,
        TargetNodeLabel = TargetNode.NodeLabel,
        TargetCriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel,
        TargetRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
    | distinct SourceNodeId, SourceNodeName, TargetNodeId, TargetNodeName, tostring(TargetCriticalityLevel), tostring(TargetRuleNames)
    // Only return devices that do not have a TPM fully enabled
    | join kind=inner no_tpm_devices on `$left.SourceNodeId == `$right.NodeId
    // Make JSON of tpm data
    | extend TpmState = tostring(bag_pack(
        'TpmSupported', TpmSupported,
        'TpmEnabled', TpmEnabled,
        'TpmActivated', TpmActivated
    ))
    // Make list of users per device
    | summarize UserList = make_list(TargetNodeName) by DeviceName
    // Count amount of exposed users per device
    | extend UserCount = array_length(UserList)
    | sort by UserCount desc
"@


    $Devices = Invoke-MtGraphSecurityQuery -Query $Query -Timespan "P1D"

    $Severity = "Medium"

    if ($return -or [string]::IsNullOrEmpty($Devices)) {
        $testResultMarkdown = "Well done. All devices with critical credentials stored are protected by TPM."
    } else {
        $testResultMarkdown = "At least one device was found with critical credentials not protected by a TPM.`n`n%TestResult%"

        Write-Verbose "Found $($Devices.Count) devices with critical credentials not protected by a TPM."

        $result = "| DeviceName | UserList | UserCount |`n"
        $result += "| --- | --- | --- |`n"
        foreach ($Device in $Devices) {
            $UserList = $($Device.UserList) -join ', '   # "user1, user2, user3"
            $result += "| $($Device.DeviceName) | $($UserList) | $($Device.UserCount) |`n"
        }
    }
    $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $result
    Add-MtTestResultDetail -Result $testResultMarkdown -Severity $Severity
    $result = [string]::IsNullOrEmpty($Devices)
    return $result
}