Detection rules › Kusto

Suspicious Service Principal creation activity

Status
available
Severity
low
Time window
1h
Group by
AppID
Source
github.com/Azure/Azure-Sentinel

This alert will detect creation of an SPN, permissions granted, credentials created, activity and deletion of the SPN in a time frame (default 10 minutes)

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Privilege EscalationT1078 Valid Accounts
Credential AccessT1528 Steal Application Access Token

Event coverage

Rule body kusto

id: 6852d9da-8015-4b95-8ecf-d9572ee0395d
name: Suspicious Service Principal creation activity
description: |
  'This alert will detect creation of an SPN, permissions granted, credentials created, activity and deletion of the SPN in a time frame (default 10 minutes)'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
      - AADServicePrincipalSignInLogs
queryFrequency: 1h
queryPeriod: 1h10m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - PrivilegeEscalation
  - InitialAccess
relevantTechniques:
  - T1078
  - T1528
query: |
  let queryfrequency = 1h;
  let wait_for_deletion = 10m;
  let account_created =
    AuditLogs 
    | where ActivityDisplayName == "Add service principal"
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend creationTime = ActivityDateTime
    | extend CreatorUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend CreatorIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);
  let account_activity =
    AADServicePrincipalSignInLogs
    | extend Activities = pack("ActivityTime", TimeGenerated ,"IpAddress", IPAddress, "ResourceDisplayName", ResourceDisplayName)
    | extend AppID = AppId
    | summarize make_list(Activities) by AppID;
  let account_deleted =
    AuditLogs 
    | where OperationName == "Remove service principal"
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend deletionTime = ActivityDateTime
    | extend DeleterUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
    | extend DeleterIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);
  let account_credentials =
    AuditLogs
    | where OperationName has_all ("Update application", "Certificates and secrets management")
    | where Result == "success"
    | extend AppID = tostring(AdditionalDetails[1].value)
    | extend credentialCreationTime = ActivityDateTime;
  let roles_assigned =
    AuditLogs
    | where ActivityDisplayName == "Add app role assignment to service principal"
    | extend AppID = tostring(TargetResources[1].displayName)
    | extend AssignedRole =  iff(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].displayName)=="AppRole.Value", tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue))),"")
    | extend AssignedRoles = pack("Role", AssignedRole)
    | summarize make_list(AssignedRoles) by AppID;
  account_created
  | where TimeGenerated between (ago(wait_for_deletion+queryfrequency)..ago(wait_for_deletion))
  | join kind= inner (account_activity) on AppID
  | join kind= inner (account_deleted) on AppID
  | join kind= inner (account_credentials) on AppID
  | join kind= inner (roles_assigned) on AppID
  | where deletionTime - creationTime between (time(0s)..wait_for_deletion)
  | extend AliveTime = deletionTime - creationTime
  | project AADTenantId, AppID, creationTime, deletionTime, CreatorUserPrincipalName, DeleterUserPrincipalName, CreatorIPAddress, DeleterIPAddress, list_Activities, list_AssignedRoles, AliveTime
  | extend CreatorName = tostring(split(CreatorUserPrincipalName, "@")[0]), CreatorUPNSuffix = tostring(split(CreatorUserPrincipalName, "@")[1])
  | extend DeleterName = tostring(split(DeleterUserPrincipalName, "@")[0]), DeleterSuffix = tostring(split(DeleterUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatorUserPrincipalName
      - identifier: Name
        columnName: CreatorName
      - identifier: UPNSuffix
        columnName: CreatorUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeleterUserPrincipalName
      - identifier: Name
        columnName: DeleterName
      - identifier: UPNSuffix
        columnName: DeleterSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CreatorIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DeleterIPAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 1h;
let wait_for_deletion = 10m;

Let binding: account_activity

let account_activity = AADServicePrincipalSignInLogs
  | extend Activities = pack("ActivityTime", TimeGenerated ,"IpAddress", IPAddress, "ResourceDisplayName", ResourceDisplayName)
  | extend AppID = AppId
  | summarize make_list(Activities) by AppID;

Let binding: account_deleted

let account_deleted = AuditLogs 
  | where OperationName == "Remove service principal"
  | where Result == "success"
  | extend AppID = tostring(AdditionalDetails[1].value)
  | extend deletionTime = ActivityDateTime
  | extend DeleterUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend DeleterIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress);

Let binding: account_credentials

let account_credentials = AuditLogs
  | where OperationName has_all ("Update application", "Certificates and secrets management")
  | where Result == "success"
  | extend AppID = tostring(AdditionalDetails[1].value)
  | extend credentialCreationTime = ActivityDateTime;

Let binding: roles_assigned

let roles_assigned = AuditLogs
  | where ActivityDisplayName == "Add app role assignment to service principal"
  | extend AppID = tostring(TargetResources[1].displayName)
  | extend AssignedRole =  iff(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].displayName)=="AppRole.Value", tostring(parse_json(tostring(parse_json(tostring(TargetResources[0].modifiedProperties))[1].newValue))),"")
  | extend AssignedRoles = pack("Role", AssignedRole)
  | summarize make_list(AssignedRoles) by AppID;

The stages below define let account_created (the rule's main pipeline source).

Stage 1: source

AuditLogs

Stage 2: where

| where ActivityDisplayName == "Add service principal"

Stage 3: where

| where Result == "success"

Stage 4: extend (4 consecutive steps)

| extend AppID = tostring(AdditionalDetails[1].value)
| extend creationTime = ActivityDateTime
| extend CreatorUserPrincipalName = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
| extend CreatorIPAddress = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)

The stages below run on account_created (the outer pipeline).

Stage 5: where

account_created
| where TimeGenerated between (ago(wait_for_deletion+queryfrequency)..ago(wait_for_deletion))

Stage 6: join

| join kind= inner (account_activity) on AppID

Stage 7: join

| join kind= inner (account_deleted) on AppID

Stage 8: join

| join kind= inner (account_credentials) on AppID

Stage 9: join

| join kind= inner (roles_assigned) on AppID

Stage 10: where

| where deletionTime - creationTime between (time(0s)..wait_for_deletion)

Stage 11: extend

| extend AliveTime = deletionTime - creationTime

Stage 12: project

| project AADTenantId, AppID, creationTime, deletionTime, CreatorUserPrincipalName, DeleterUserPrincipalName, CreatorIPAddress, DeleterIPAddress, list_Activities, list_AssignedRoles, AliveTime

Stage 13: extend

| extend CreatorName = tostring(split(CreatorUserPrincipalName, "@")[0]), CreatorUPNSuffix = tostring(split(CreatorUserPrincipalName, "@")[1])

Stage 14: extend

| extend DeleterName = tostring(split(DeleterUserPrincipalName, "@")[0]), DeleterSuffix = tostring(split(DeleterUserPrincipalName, "@")[1])

Stage 15: summarize

summarize by AppID

Stage 16: summarize

summarize by AppID

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
ActivityDisplayNameeq
  • Add app role assignment to service principal transforms: cased
  • Add service principal transforms: cased
OperationNameeq
  • Remove service principal transforms: cased
OperationNamematch
  • Certificates and secrets management
  • Update application
Resulteq
  • success transforms: cased

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
AppIDsummarize