Detection rules › Kusto

Hunt for local AI Agent activity with Agent Info

Group by
AgentId, ExtractedObservabilityID, ObservabilityID, SourceAIAgentId, SourceAgentId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This query allows you to find the activity a local AI Agent performed via the CloudAppEvents table using the AgentsInfo and ExposureGraphNodes table.

References

Telemetry coverage

Rule body

// Fill in agent name or one of the Agent IDs you have found
let agent_name = "";
let some_agent_id = "";
AgentsInfo 
| where TimeGenerated > ago(1d)
| summarize arg_max(TimeGenerated, *) by AgentId
| where (isempty(some_agent_id) and Name =~ agent_name) or (isempty(agent_name) and * has some_agent_id)
// Take local AI Agents
| where Platform == "LocalAgents"
| distinct Name, Platform, SourceAgentId
// Join with Graph Nodes to get the ObservabilityID
| join kind=inner (
    ExposureGraphNodes
    | where NodeLabel == "ai-agent"
    | where parse_json(NodeProperties).rawData.aiAgentMetadata.platform == "LocalAgents"
    | extend SourceAIAgentId = extract("{\"type\":\"SourceAIAgentId\",\"id\":\"([^\"]+)\"}", 1, tostring(EntityIds))
    | extend A365RegistryAIAgentId = extract("{\"type\":\"A365RegistryAIAgentId\",\"id\":\"tenantid=([^;]+);titleid=([^\"]+)\"}", 2, tostring(EntityIds))
    | project SourceAIAgentId, A365RegistryAIAgentId
) on $left.SourceAgentId == $right.SourceAIAgentId
| extend ExtractedObservabilityID = iff(
    A365RegistryAIAgentId matches regex @"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})",
    extract(@"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 1, A365RegistryAIAgentId),
    A365RegistryAIAgentId
)
| join kind=inner (
    CloudAppEvents
    | where ActionType in ("InvokeAgent","InferenceCall","ExecuteToolBySDK","ExecuteToolByGateway","ExecuteToolByMCPServer")
    // Extract the platformIDs and ObservabilityID
    | extend PlatformAgentId = tostring(parse_json(RawEventData)["PlatformAgentId"]), 
        PlatformTargetAgentId = tostring(parse_json(RawEventData)["PlatformTargetAgentId"])
    | extend PlatformId = iff(isempty(PlatformAgentId) and isnotempty(PlatformTargetAgentId), PlatformTargetAgentId, PlatformAgentId)
    | extend ObservabilityID = iff(
        PlatformId matches regex @"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 
        extract(@"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 1, PlatformId),
        PlatformId
    )
) on $left.ExtractedObservabilityID == $right.ObservabilityID

Stages and Predicates

Parameters

let agent_name = "";
let some_agent_id = "";

Stage 1: source

AgentsInfo

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by AgentId

Stage 4: where

| where (isempty(some_agent_id) and Name =~ agent_name) or (isempty(agent_name) and * has some_agent_id)

Stage 5: where

| where Platform == "LocalAgents"

Stage 6: distinct

| distinct Name, Platform, SourceAgentId

Stage 7: join

| join kind=inner (
    ExposureGraphNodes
    | where NodeLabel == "ai-agent"
    | where parse_json(NodeProperties).rawData.aiAgentMetadata.platform == "LocalAgents"
    | extend SourceAIAgentId = extract("{\"type\":\"SourceAIAgentId\",\"id\":\"([^\"]+)\"}", 1, tostring(EntityIds))
    | extend A365RegistryAIAgentId = extract("{\"type\":\"A365RegistryAIAgentId\",\"id\":\"tenantid=([^;]+);titleid=([^\"]+)\"}", 2, tostring(EntityIds))
    | project SourceAIAgentId, A365RegistryAIAgentId
) on $left.SourceAgentId == $right.SourceAIAgentId

Stage 8: extend

| extend ExtractedObservabilityID = iff(
    A365RegistryAIAgentId matches regex @"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})",
    extract(@"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 1, A365RegistryAIAgentId),
    A365RegistryAIAgentId
)
ExtractedObservabilityID =
ifA365RegistryAIAgentId matches regex @"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})"extract(@"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 1, A365RegistryAIAgentId)
elseA365RegistryAIAgentId

Stage 9: join

| join kind=inner (
    CloudAppEvents
    | where ActionType in ("InvokeAgent","InferenceCall","ExecuteToolBySDK","ExecuteToolByGateway","ExecuteToolByMCPServer")
    | extend PlatformAgentId = tostring(parse_json(RawEventData)["PlatformAgentId"]), 
        PlatformTargetAgentId = tostring(parse_json(RawEventData)["PlatformTargetAgentId"])
    | extend PlatformId = iff(isempty(PlatformAgentId) and isnotempty(PlatformTargetAgentId), PlatformTargetAgentId, PlatformAgentId)
    | extend ObservabilityID = iff(
        PlatformId matches regex @"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 
        extract(@"(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})", 1, PlatformId),
        PlatformId
    )
) on $left.ExtractedObservabilityID == $right.ObservabilityID

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypein
  • ExecuteToolByGateway
  • ExecuteToolByMCPServer
  • ExecuteToolBySDK
  • InferenceCall
  • InvokeAgent
field:"ActionType" kind:in
NodeLabeleq
  • ai-agent
field:"NodeLabel" kind:eq value:"ai-agent"
Platformeq
  • LocalAgents
field:"Platform" kind:eq value:"LocalAgents"
platformeq
  • LocalAgents
field:"platform" kind:eq value:"LocalAgents"

Output fields

These fields are emitted when the rule matches.

FieldSource
AgentIdsummarize
ExtractedObservabilityIDextend