AI Agents - Instructions changed on previously published agent


Description

Identifies instruction changes on AI agents that were published in both current and baseline snapshots. Review the changed prompt and audit records to confirm whether the update was authorized. Run within 2 days of a change to retain coverage.

Query · kql

let lookback = 14d;
let recent = 2d;
let IdentityIdtoUPN = materialize(
    IdentityInfo
    | extend ResolvedAccountUpn = tostring(column_ifexists("AccountUpn", column_ifexists("AccountUPN", ""))),
             IdentityTimestamp = todatetime(column_ifexists("Timestamp", column_ifexists("TimeGenerated", datetime(null))))
    | where IdentityTimestamp >= ago(lookback)
    | where isnotempty(AccountObjectId) and isnotempty(ResolvedAccountUpn)
    | summarize arg_max(IdentityTimestamp, ResolvedAccountUpn) by AccountObjectId
    | project AccountObjectId = tostring(AccountObjectId), AccountUpn = ResolvedAccountUpn);
let CurrentState =
    AgentsInfo
    | where Timestamp > ago(recent)
    | summarize arg_max(Timestamp, *) by AgentId
    | where LifecycleStatus != "Deleted"
    | where PublishedStatus == "Published"
    | where isnotempty(Instructions) and Instructions != "N/A"
    | project AgentId, Timestamp, Name, Platform, CreatedDateTime, Owners,
              CurrentInstructions = Instructions;
let BaselineState =
    AgentsInfo
    | where Timestamp between (ago(lookback) .. ago(recent))
    | where LifecycleStatus != "Deleted"
    | summarize arg_max(Timestamp, *) by AgentId
    | where PublishedStatus == "Published"
    | where isnotempty(Instructions) and Instructions != "N/A"
    | project AgentId, PreviousTimestamp = Timestamp,
              PreviousInstructions = Instructions;
CurrentState
| join kind=inner BaselineState on AgentId
| where CurrentInstructions != PreviousInstructions
| extend PreviousInstructionsHash = hash_sha256(PreviousInstructions),
         CurrentInstructionsHash = hash_sha256(CurrentInstructions),
         InstructionsLengthDelta = strlen(CurrentInstructions) - strlen(PreviousInstructions)
| extend OwnerIds = iff(array_length(coalesce(Owners, dynamic([]))) > 0, Owners, dynamic([""]))
| mv-expand OwnerId = OwnerIds to typeof(string)
| join kind=leftouter IdentityIdtoUPN on $left.OwnerId == $right.AccountObjectId
| project-rename OwnerUpn = AccountUpn
| extend OwnerAccountName = tostring(split(OwnerUpn, "@")[0]),
         OwnerAccountUPNSuffix = tostring(split(OwnerUpn, "@")[1])
| project Timestamp, PreviousTimestamp, AgentId, Name, Platform, CreatedDateTime,
         PreviousInstructionsHash, CurrentInstructionsHash, InstructionsLengthDelta,
         OwnerId, OwnerUpn, OwnerAccountName, OwnerAccountUPNSuffix
| sort by Timestamp desc
Raw source AI Agents - Instructions changed on previously published agent · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
id: 662eae7f-494f-41a8-bfef-23dd80361795
name: AI Agents - Instructions changed on previously published agent
description: |
  Identifies instruction changes on AI agents that were published in both current and baseline snapshots. Review the changed prompt and audit records to confirm whether the update was authorized. Run within 2 days of a change to retain coverage.
requiredDataConnectors: []
tactics:
  - Impact
relevantTechniques:
  - T1565.001
query: |
  let lookback = 14d;
  let recent = 2d;
  let IdentityIdtoUPN = materialize(
      IdentityInfo
      | extend ResolvedAccountUpn = tostring(column_ifexists("AccountUpn", column_ifexists("AccountUPN", ""))),
               IdentityTimestamp = todatetime(column_ifexists("Timestamp", column_ifexists("TimeGenerated", datetime(null))))
      | where IdentityTimestamp >= ago(lookback)
      | where isnotempty(AccountObjectId) and isnotempty(ResolvedAccountUpn)
      | summarize arg_max(IdentityTimestamp, ResolvedAccountUpn) by AccountObjectId
      | project AccountObjectId = tostring(AccountObjectId), AccountUpn = ResolvedAccountUpn);
  let CurrentState =
      AgentsInfo
      | where Timestamp > ago(recent)
      | summarize arg_max(Timestamp, *) by AgentId
      | where LifecycleStatus != "Deleted"
      | where PublishedStatus == "Published"
      | where isnotempty(Instructions) and Instructions != "N/A"
      | project AgentId, Timestamp, Name, Platform, CreatedDateTime, Owners,
                CurrentInstructions = Instructions;
  let BaselineState =
      AgentsInfo
      | where Timestamp between (ago(lookback) .. ago(recent))
      | where LifecycleStatus != "Deleted"
      | summarize arg_max(Timestamp, *) by AgentId
      | where PublishedStatus == "Published"
      | where isnotempty(Instructions) and Instructions != "N/A"
      | project AgentId, PreviousTimestamp = Timestamp,
                PreviousInstructions = Instructions;
  CurrentState
  | join kind=inner BaselineState on AgentId
  | where CurrentInstructions != PreviousInstructions
  | extend PreviousInstructionsHash = hash_sha256(PreviousInstructions),
           CurrentInstructionsHash = hash_sha256(CurrentInstructions),
           InstructionsLengthDelta = strlen(CurrentInstructions) - strlen(PreviousInstructions)
  | extend OwnerIds = iff(array_length(coalesce(Owners, dynamic([]))) > 0, Owners, dynamic([""]))
  | mv-expand OwnerId = OwnerIds to typeof(string)
  | join kind=leftouter IdentityIdtoUPN on $left.OwnerId == $right.AccountObjectId
  | project-rename OwnerUpn = AccountUpn
  | extend OwnerAccountName = tostring(split(OwnerUpn, "@")[0]),
           OwnerAccountUPNSuffix = tostring(split(OwnerUpn, "@")[1])
  | project Timestamp, PreviousTimestamp, AgentId, Name, Platform, CreatedDateTime,
           PreviousInstructionsHash, CurrentInstructionsHash, InstructionsLengthDelta,
           OwnerId, OwnerUpn, OwnerAccountName, OwnerAccountUPNSuffix
  | sort by Timestamp desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: OwnerAccountName
      - identifier: UPNSuffix
        columnName: OwnerAccountUPNSuffix
      - identifier: AadUserId
        columnName: OwnerId
version: 1.0.0

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.