Detection rules › Kusto

Hunt for critical credentials on non-TPM enabled devices

Group by
DeviceName, NodeId, OnboardingStatus, SourceNodeId, TpmState
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This query searches for devices that does not have a TPM (Trusted Platform Module) enabled but contains critical credentials. The output shows for how many users each non-TPM device has credentials, together with the rules why each user is considered a critical user.

Telemetry coverage

Rule body

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)
    // 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 does 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 JSON of users data
| extend Json = bag_pack(
    'User', TargetNodeName,
    'UserCriticalityLevel', TargetCriticalityLevel,
    'UserRuleNames', TargetRuleNames
)
// Make list of users per device
| summarize UserList = make_list(Json) by DeviceName, OnboardingStatus, TpmState
// Count amount of exposed users per device
| extend UserCount = array_length(UserList)
| sort by UserCount desc

Stages and Predicates

Let binding: no_tpm_devices used in Stages 1, 6

let no_tpm_devices = (
    ExposureGraphNodes
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    | 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)
    | distinct NodeId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    | 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 binding: critical_users

let critical_users = toscalar(
    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)
);

Stage 1: source

let no_tpm_devices

Stage 2: source

ExposureGraphEdges

Stage 3: macro

macro

Stage 4: macro

macro

Stage 5: distinct

distinct SourceNodeId, SourceNodeName, TargetNodeId, TargetNodeName

Stage 6: join

join kind=inner (no_tpm_devices) on SourceNodeId, NodeId

Stage 7: extend

extend TpmState

Stage 8: extend

extend Json

Stage 9: summarize

summarize UserList by DeviceName, OnboardingStatus, TpmState

Stage 10: extend

extend UserCount

Stage 11: sort

sort by UserCount

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
DeviceNamesummarize
OnboardingStatussummarize
TpmStatesummarize
UserListsummarize
UserCountextend