public/xspm/Test-MtXspmCriticalCredsOnDevicesWithNonCriticalAccounts.ps1

<#
.SYNOPSIS
    Test to find devices with critical and non-critical user credentials on the same device

.DESCRIPTION
    Test to find devices with critical and non-critical user credentials on the same device

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

.EXAMPLE
    Test-MtXspmCriticalCredsOnDevicesWithNonCriticalAccounts

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


function Test-MtXspmCriticalCredsOnDevicesWithNonCriticalAccounts {
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This test checks for devices with critical and non-critical user credentials on the same device.')]
    [OutputType([bool])]
    param()

    Write-Verbose "Get raw data from Exposure Management..."
    $Query = "
        // Search for all users and save their criticality level
        let xspm_users = materialize(
            ExposureGraphNodes
            | where NodeLabel == 'user'
            | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
            | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
            | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
        );
        // Make a list of all critical users
        let critical_users = toscalar(
            xspm_users
            | where CriticalityLevel == 0
            | summarize make_set(NodeName)
        );
        // Make a list of all non critical users
        let non_critical_users = toscalar(
            xspm_users
            | where CriticalityLevel != 0
            | summarize make_set(NodeName)
        );
        ExposureGraphEdges
        // Focus on credential related paths
        | where EdgeLabel in ('contains', 'can impersonate as', 'has credentials of', 'can authenticate as', 'has permissions to', 'frequently logged in by')
        // Make graph for max of 3 edges, where we start from a device and end with an user
        | 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'
            project DeviceName = SourceNode.NodeName,
            Edges = anyEdge.EdgeLabel,
            TargetNodeName = TargetNode.NodeName,
            TargetNodeLabel = TargetNode.NodeLabel
        // Make a list of all users a device has credentials for
        | summarize UserList = make_set(TargetNodeName) by DeviceName
        // Only return devices with more than one credential
        | where array_length(UserList) > 1
        // Make new lists saving the critical users and non critical users per device
        | extend CriticalUserList = set_intersect(UserList, critical_users),
            NonCriticalUserList = set_intersect(UserList, non_critical_users)
        // Flag when a device has both critical and non critical users
        | where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0
        // Sort and Remove the total user list
        | sort by array_length(UserList) desc
        | project-away UserList
    "

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

    $Severity = "Low"

    if ([string]::IsNullOrEmpty($Devices)) {
        $testResultMarkdown = "Well done. No devices with shared critical and non-critical user credentials are found."
    } else {
        $testResultMarkdown = "At least one device with shared critical and non-critical user credentials were found.`n`n%TestResult%"

        Write-Verbose "Found $($Devices.Count) devices sharing critical and non-critical credentials on the same device."

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