Detection rules › Kusto

Mass secret retrieval from Azure Key Vault

Status
available
Severity
low
Time window
1d
Group by
CallerObjectId, CallerObjectUPN, ResourceType, ResultType, identity_claim_appid_g
Source
github.com/Azure/Azure-Sentinel

'Identifies mass secret retrieval from Azure Key Vault observed by a single user. Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications. You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'

MITRE ATT&CK coverage

TacticTechniques
Credential AccessT1003 OS Credential Dumping

Rule body kusto

id: 24f8c234-d1ff-40ec-8b73-96b17a3a9c1c
name: Mass secret retrieval from Azure Key Vault
description: |
  'Identifies mass secret retrieval from Azure Key Vault observed by a single user. 
  Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications. 
  You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureKeyVault
    dataTypes:
      - KeyVaultData
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1003
query: |
  let DistinctSecretsThreshold = 10;
  let EventCountThreshold = 50;
  // To avoid any False Positives, filtering using AppId is recommended.
  // The AppId 509e4652-da8d-478d-a730-e9d4a1996ca4 has been added in the query as it corresponds to Azure Resource Graph performing VaultGet operations for indexing and syncing all tracked resources across Azure.
  // The AppId 8cae6e77-e04e-42ce-b5cb-50d82bce26b1 has been added as it correspond to Microsoft Policy Insights Provider Data Plane performing VaultGet operations for policies checks.
  let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
  let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);
  AzureDiagnostics
  | where OperationName in (OperationList) and ResourceType =~ "VAULTS"
  | where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')
  | extend
      ResourceId,
      ResultType = column_ifexists("ResultType", ""),
      identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
      identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
      identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
      identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")
  | extend
      CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
      CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
  | as _Retrievals
  | where CallerObjectId in (toscalar(
      _Retrievals
      | where ResultType == "Success"
      | summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
      | where Count > DistinctSecretsThreshold
      | summarize make_set(CallerObjectId,10000)
  ))
  | extend
      requestUri_s = column_ifexists("requestUri_s", ""),
      id_s = column_ifexists("id_s", ""),
      CallerIPAddress = column_ifexists("CallerIPAddress", ""),
      clientInfo_s = column_ifexists("clientInfo_s", "")
  | summarize
      EventCount = count(),
      StartTime = min(TimeGenerated),
      EndTime = max(TimeGenerated),
      ResourceList = make_set(Resource, 50),
      OperationNameList = make_set(OperationName, 50),
      RequestURLList = make_set(requestUri_s, 50),
      ResourceId = max(ResourceId),
      CallerIPList = make_set(CallerIPAddress, 50),
      clientInfo_sList = make_set(clientInfo_s, 50),
      CallerIPMax = max(CallerIPAddress)
      by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN
      | where EventCount > EventCountThreshold
  | project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList
  | extend timestamp = EndTime
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: CallerObjectId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIPMax
version: 1.0.8
kind: Scheduled

Stages and Predicates

Parameters

let DistinctSecretsThreshold = 10;
let EventCountThreshold = 50;
let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);

Stage 1: source

AzureDiagnostics

Stage 2: where

| where OperationName in (OperationList) and ResourceType =~ "VAULTS"

Stage 3: where

| where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')

Stage 4: extend

| extend
    ResourceId,
    ResultType = column_ifexists("ResultType", ""),
    identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
    identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
    identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
    identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")

Stage 5: extend

| extend
    CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
    CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
CallerObjectId =
ifisempty(identity_claim_oid_g)identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g
elseidentity_claim_oid_g
CallerObjectUPN =
ifisempty(identity_claim_upn_s)identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s
elseidentity_claim_upn_s

Stage 6: kusto:as

| as _Retrievals

Stage 7: where

| where CallerObjectId in (toscalar(
    _Retrievals
    | where ResultType == "Success"
    | summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
    | where Count > DistinctSecretsThreshold
    | summarize make_set(CallerObjectId,10000)
))

Stage 8: extend

| extend
    requestUri_s = column_ifexists("requestUri_s", ""),
    id_s = column_ifexists("id_s", ""),
    CallerIPAddress = column_ifexists("CallerIPAddress", ""),
    clientInfo_s = column_ifexists("clientInfo_s", "")

Stage 9: summarize

| summarize
    EventCount = count(),
    StartTime = min(TimeGenerated),
    EndTime = max(TimeGenerated),
    ResourceList = make_set(Resource, 50),
    OperationNameList = make_set(OperationName, 50),
    RequestURLList = make_set(requestUri_s, 50),
    ResourceId = max(ResourceId),
    CallerIPList = make_set(CallerIPAddress, 50),
    clientInfo_sList = make_set(clientInfo_s, 50),
    CallerIPMax = max(CallerIPAddress)
    by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN
Threshold
gt 50

Stage 10: where

| where EventCount > EventCountThreshold

Stage 11: project-reorder

| project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList

Stage 12: extend

| extend timestamp = EndTime

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
OperationNameeqVaultGet
identity_claim_appid_gin509e4652-da8d-478d-a730-e9d4a1996ca4, 8cae6e77-e04e-42ce-b5cb-50d82bce26b1

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
EventCountgt
  • 50 transforms: cased
OperationNamein
  • KeyGet transforms: cased
  • SecretGet transforms: cased
  • VaultGet transforms: cased
ResourceTypeeq
  • VAULTS

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
CallerIPListsummarize
CallerIPMaxsummarize
CallerObjectIdsummarize
CallerObjectUPNsummarize
EndTimesummarize
EventCountsummarize
OperationNameListsummarize
RequestURLListsummarize
ResourceIdsummarize
ResourceListsummarize
ResourceTypesummarize
ResultTypesummarize
StartTimesummarize
clientInfo_sListsummarize
identity_claim_appid_gsummarize
timestampextend