Detection rules › Kusto

Kusto rules: 2e

AWSCloudTrail - Creating keys with encrypt policy without MFA

#
Status
available
Severity
medium
Time window
1d
Group by
AwsEventId
Source
github.com/Azure/Azure-Sentinel

Identifies AWS KMS key creation or policy changes that allow kms:Encrypt or kms:* to any principal. This configuration can expose encryption capability outside approved boundaries and should be investigated as overly permissive or malicious.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 454133a7-5427-4a7c-bdc4-0adfa84dda16
name: AWSCloudTrail - Creating keys with encrypt policy without MFA
description: |
  Identifies AWS KMS key creation or policy changes that allow `kms:Encrypt` or `kms:*` to any principal. This configuration can expose encryption capability outside approved boundaries and should be investigated as overly permissive or malicious.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
    let check_actions = AWSCloudTrail
    | where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
    | extend state=parse_json(parse_json(replace_string(tostring(parse_json(RequestParameters)['policy']),'\\"','"')).['Statement'])
    | mv-expand state
    | extend Action= tostring(parse_json(state.['Action'][0])), Effect=tostring(parse_json(state.['Effect'])),
             Principal=tostring(parse_json(state.['Principal']))
    | where Action in ("kms:Encrypt", "kms:*") and (Effect == 'Allow') and (Principal has "*")
    | distinct AwsEventId;
    AWSCloudTrail
    | where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
    | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
    | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
    | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
    | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
      AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
    | join kind=inner (check_actions) on AwsEventId
    | extend timestamp = TimeGenerated
    | project-away AwsEventId1
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  AwsEventId: AwsEventId
  EventName: EventName
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS KMS key creation or policy update by {{AccountName}} without MFA'
  alertDescriptionFormat: 'KMS key creation or policy update by {{AccountName}} without MFA resulted in permissive encrypt access. AwsEventId: {{AwsEventId}}'
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: check_actions used in Stage 4

let check_actions = AWSCloudTrail
| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
| extend state=parse_json(parse_json(replace_string(tostring(parse_json(RequestParameters)['policy']),'\\"','"')).['Statement'])
| mv-expand state
| extend Action= tostring(parse_json(state.['Action'][0])), Effect=tostring(parse_json(state.['Effect'])),
         Principal=tostring(parse_json(state.['Principal']))
| where Action in ("kms:Encrypt", "kms:*") and (Effect == 'Allow') and (Principal has "*")
| distinct AwsEventId;

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("CreateKey", "PutKeyPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 4: join

| join kind=inner (check_actions) on AwsEventId

Stage 5: extend

extend timestamp

Stage 6: project-away

project-away AwsEventId1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionin
  • kms:*
  • kms:Encrypt
field:"Action" kind:in
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"ErrorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"ErrorMessage" kind:is_null
EventNamein
  • CreateKey
  • CreateKey
  • PutKeyPolicy
  • PutKeyPolicy
field:"aws::eventName" kind:in
Principalmatch
  • * transforms: term
field:"Principal" kind:match value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend
timestampextend

Azure DevOps Pipeline modified by a new user

#
Status
available
Severity
medium
Time window
14d
Group by
$left.ActorUserId, $right.AadUserId, AadUserId
Source
github.com/Azure/Azure-Sentinel

'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to. This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'

MITRE ATT&CK Coverage

TacticTechniques
Execution
Defense Impairment

Rule Body

id: 155e9134-d5ad-4a6f-88f3-99c220040b66
name: Azure DevOps Pipeline modified by a new user
description: |
  'There are several potential pipeline steps that could be modified by an attacker to inject malicious code into the build cycle. A likely attacker path is the modification to an existing pipeline that they have access to. 
  This detection looks for users modifying a pipeline when they have not previously been observed modifying or creating that pipeline before. This query also joins events with data to Microsoft Entra ID Protection in order to show if the user conducting the action has any associated Microsoft Entra ID Protection alerts. You can also choose to filter this detection to only alert when the user also has Microsoft Entra ID Protection alerts associated with them.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution 
  - DefenseEvasion
relevantTechniques:
  - T1578
  - T1569
query: |
  // Set the lookback to determine if user has created pipelines before
  let timeback = 14d;
  // Set the period for detections
  let timeframe = 1d;
  // Get a list of previous Release Pipeline creators to exclude
  let releaseusers = ADOAuditLogs
  | where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
  | where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
  // We want to look for users performing actions in specific projects so we create this userscope object to match on
  | extend UserScope = strcat(ActorUserId, "-", ProjectName)
  | summarize by UserScope;
  // Get Release Pipeline creations by new users
  ADOAuditLogs
  | where TimeGenerated > ago(timeframe)
  | where OperationName =~ "Release.ReleasePipelineModified"
  | extend UserScope = strcat(ActorUserId, "-", ProjectName)
  | where UserScope !in (releaseusers)
  | extend ActorUPN = tolower(ActorUPN)
  | project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope
  // See if any of these users have Azure AD alerts associated with them in the same timeframe
  | join kind = leftouter (
  SecurityAlert
  | where TimeGenerated > ago(timeframe)
  | where ProviderName == "IPC"
  | extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
  | summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId
  | extend Alerts = iif(isnotempty(Alerts), Alerts, 0)
  // Uncomment the line below to only show results where the user as AADIdP alerts
  //| where Alerts > 0
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.8
kind: Scheduled

Stages and Predicates

Parameters

let timeback = 14d;
let timeframe = 1d;

Let Binding: releaseusers used in Stage 5

let releaseusers = ADOAuditLogs
| where TimeGenerated > ago(timeback) and TimeGenerated < ago(timeframe)
| where OperationName in ("Release.ReleasePipelineCreated", "Release.ReleasePipelineModified")
| extend UserScope = strcat(ActorUserId, "-", ProjectName)
| summarize by UserScope;

Stage 1: source

ADOAuditLogs

Stage 2: where

| where TimeGenerated > ago(timeframe)

Stage 3: where

| where OperationName =~ "Release.ReleasePipelineModified"

Stage 4: extend

| extend UserScope = strcat(ActorUserId, "-", ProjectName)

Stage 5: where

| where UserScope !in (releaseusers)

Stage 6: extend

| extend ActorUPN = tolower(ActorUPN)

Stage 7: project-away

| project-away Id, ActivityId, ActorCUID, ScopeId, ProjectId, TenantId, SourceSystem, UserScope

Stage 8: join

| join kind = leftouter (
SecurityAlert
| where TimeGenerated > ago(timeframe)
| where ProviderName == "IPC"
| extend AadUserId = tostring(parse_json(Entities)[0].AadUserId)
| summarize Alerts=count() by AadUserId) on $left.ActorUserId == $right.AadUserId

Stage 9: extend (3 consecutive steps)

extend AccountName, AccountUPNSuffix, Alerts, timestamp

Stage 10: summarize aggregation inside the join branch

summarize by AadUserId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserScopeeqreleaseusersexcludes:UserScope field:"UserScope" value:"releaseusers"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AadUserIdsummarize

CDM_ContinuousDiagnostics&Mitigation_PostureChanged

#
Status
available
Severity
medium
Time window
7d
Group by
AssessedResourceId, MaturityLevel, RecommendationDisplayName
Source
github.com/Azure/Azure-Sentinel

'This alert is designed to monitor Azure policies aligned with the Continuous Diagnostics & Mitigation (CDM) Program. The alert triggers when policy compliance falls below 70% within a 1 week timeframe.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: fd950af9-d9db-4879-a60a-7267cc041beb
name: CDM_ContinuousDiagnostics&Mitigation_PostureChanged
description: |
  'This alert is designed to monitor Azure policies aligned with the Continuous Diagnostics & Mitigation (CDM) Program. The alert triggers when policy compliance falls below 70% within a 1 week timeframe.'
severity: Medium
status: Available 
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  SecurityRecommendation
  | where RecommendationDisplayName <> ""
  | extend MaturityLevel=iff(RecommendationDisplayName has_any("log", "defender", "asset", "arc"), "Hardware Asset Management (HWAM)",
      iff(RecommendationDisplayName has_any("allow", "software", "application"), "Software Asset Management (SWAM)",
      iff(RecommendationDisplayName has_any("account", "user", "identity", "trust"), "User Trust (TRUST)",
      iff(RecommendationDisplayName has_any("access", "auth", "key", "cert", "token"), "Credentials & Authenticators (CRED)",
      iff(RecommendationDisplayName has_any("priv", "admin", "root"), "Privileges (PRIV)",
      iff(RecommendationDisplayName has_any("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http"), "Boundary Protection (BOUND)",
      iff(RecommendationDisplayName has_any("event", "agent", "incident", "back", "privacy", "audit", "collect"), "Manage Events (MNGEVT)",
      iff(RecommendationDisplayName has_any("edr", "malware", "endpoint protection", "detect", "respon"), "Endpoint Detection & Response (EDR)",
      iff(RecommendationDisplayName has_any("aks", "contain", "kube", "supply"), "Design & Build in Security (DBS)",
      iff(RecommendationDisplayName has_any("data", "storage", "sql", "cmk", "key"), "Data Protection Management (DPM)", "Other"))))))))))
  | where MaturityLevel in ('Hardware Asset Management (HWAM)', 'Software Asset Management (SWAM)', 'User Trust (TRUST)', 'Credentials & Authenticators (CRED)', 'Privileges (PRIV)', 'Boundary Protection (BOUND)', 'Manage Events (MNGEVT)', 'Endpoint Detection & Response (EDR)', 'Design & Build in Security (DBS)', 'Data Protection Management (DPM)')
  | summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName
  | summarize
      Failed = countif(RecommendationState == "Unhealthy"),
      Passed = countif(RecommendationState == "Healthy"),
      NotApplicable = countif(RecommendationState == "NotApplicable" or RecommendationState == "Removed"),
      Applicable = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy"),
      Total = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy" or RecommendationState == "NotApplicable" or RecommendationState == "Removed")
      by MaturityLevel
  | extend PassedControls = (Passed / todouble(Applicable)) * 100
  | project MaturityLevel, Total, PassedControls, Passed, Failed, Applicable, NotApplicable
  | sort by Total, Passed desc
  | extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
  | project CapabilityArea=MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()
  | where CapabilityArea <> ''
  | where PassedControls < 70 
  //Adjust Either FailedRatePercentage or PassedRatePercentage Thresholds within organizational requirements
  | sort by PassedControls desc
  | extend URLCustomEntity = RemediationLink
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityRecommendation

Stage 2: where

| where RecommendationDisplayName <> ""

Stage 3: extend

| extend MaturityLevel=iff(RecommendationDisplayName has_any("log", "defender", "asset", "arc"), "Hardware Asset Management (HWAM)",
    iff(RecommendationDisplayName has_any("allow", "software", "application"), "Software Asset Management (SWAM)",
    iff(RecommendationDisplayName has_any("account", "user", "identity", "trust"), "User Trust (TRUST)",
    iff(RecommendationDisplayName has_any("access", "auth", "key", "cert", "token"), "Credentials & Authenticators (CRED)",
    iff(RecommendationDisplayName has_any("priv", "admin", "root"), "Privileges (PRIV)",
    iff(RecommendationDisplayName has_any("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http"), "Boundary Protection (BOUND)",
    iff(RecommendationDisplayName has_any("event", "agent", "incident", "back", "privacy", "audit", "collect"), "Manage Events (MNGEVT)",
    iff(RecommendationDisplayName has_any("edr", "malware", "endpoint protection", "detect", "respon"), "Endpoint Detection & Response (EDR)",
    iff(RecommendationDisplayName has_any("aks", "contain", "kube", "supply"), "Design & Build in Security (DBS)",
    iff(RecommendationDisplayName has_any("data", "storage", "sql", "cmk", "key"), "Data Protection Management (DPM)", "Other"))))))))))
MaturityLevel =
if(RecommendationDisplayName has "log" or RecommendationDisplayName has "defender" or RecommendationDisplayName has "asset" or RecommendationDisplayName has "arc")"Hardware Asset Management (HWAM)"
elseiff((RecommendationDisplayName has_any ("allow", "software", "application")), "Software Asset Management (SWAM)", iff((RecommendationDisplayName has_any ("account", "user", "identity", "trust")), "User Trust (TRUST)", iff((RecommendationDisplayName has_any ("access", "auth", "key", "cert", "token")), "Credentials & Authenticators (CRED)", iff((RecommendationDisplayName has_any ("priv", "admin", "root")), "Privileges (PRIV)", iff((RecommendationDisplayName has_any ("network", "internet", "traffic", "firewall", "intrusion", "bound", "tls", "gateway", "subnet", "web", "url", "proxy", "just", "port", "JIT", "http")), "Boundary Protection (BOUND)", iff((RecommendationDisplayName has_any ("event", "agent", "incident", "back", "privacy", "audit", "collect")), "Manage Events (MNGEVT)", iff((RecommendationDisplayName has_any ("edr", "malware", "endpoint protection", "detect", "respon")), "Endpoint Detection & Response (EDR)", iff((RecommendationDisplayName has_any ("aks", "contain", "kube", "supply")), "Design & Build in Security (DBS)", iff((RecommendationDisplayName has_any ("data", "storage", "sql", "cmk", "key")), "Data Protection Management (DPM)", "Other")))))))))

Stage 4: where

| where MaturityLevel in ('Hardware Asset Management (HWAM)', 'Software Asset Management (SWAM)', 'User Trust (TRUST)', 'Credentials & Authenticators (CRED)', 'Privileges (PRIV)', 'Boundary Protection (BOUND)', 'Manage Events (MNGEVT)', 'Endpoint Detection & Response (EDR)', 'Design & Build in Security (DBS)', 'Data Protection Management (DPM)')

Stage 5: summarize

| summarize arg_max(TimeGenerated, *) by AssessedResourceId, RecommendationDisplayName

Stage 6: summarize

| summarize
    Failed = countif(RecommendationState == "Unhealthy"),
    Passed = countif(RecommendationState == "Healthy"),
    NotApplicable = countif(RecommendationState == "NotApplicable" or RecommendationState == "Removed"),
    Applicable = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy"),
    Total = countif(RecommendationState == "Unhealthy" or RecommendationState == "Healthy" or RecommendationState == "NotApplicable" or RecommendationState == "Removed")
    by MaturityLevel

Stage 7: extend

| extend PassedControls = (Passed / todouble(Applicable)) * 100

Stage 8: project

| project MaturityLevel, Total, PassedControls, Passed, Failed, Applicable, NotApplicable

Stage 9: sort

| sort by Total, Passed desc

Stage 10: extend

| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')

Stage 11: project

| project CapabilityArea=MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()

Stage 12: where

| where CapabilityArea <> ''

Stage 13: where

| where PassedControls < 70

Stage 14: sort

| sort by PassedControls desc

Stage 15: extend

| extend URLCustomEntity = RemediationLink

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MaturityLevelin
  • Boundary Protection (BOUND)
  • Credentials & Authenticators (CRED)
  • Data Protection Management (DPM)
  • Design & Build in Security (DBS)
  • Endpoint Detection & Response (EDR)
  • Hardware Asset Management (HWAM)
  • Manage Events (MNGEVT)
  • Privileges (PRIV)
  • Software Asset Management (SWAM)
  • User Trust (TRUST)
field:"MaturityLevel" kind:in
PassedControlslt
  • 70 transforms: number
field:"PassedControls" kind:lt value:"70"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CapabilityAreaproject
Failedproject
LastObservedproject
Passedproject
PassedControlsproject
RemediationLinkproject
Totalproject
URLCustomEntityextend

Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed

#
Status
available
Severity
medium
Time window
2d
Source
github.com/Azure/Azure-Sentinel

Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Outbound Collaboration Settings are changed for "Users & Groups" and for "Applications".

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 229f71ba-d83b-42a5-b83b-11a641049ed1
name: Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed
description: |
  'Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Outbound Collaboration Settings are changed for "Users & Groups" and for "Applications".'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
  - Discovery
relevantTechniques:
  - T1078.004
  - T1136.003
  - T1087.004
query: |
  // In User & Groups and in Applications, the following "AccessType" values in columns PremodifiedOutboundSettings and ModifiedOutboundSettings are interpreted accordingly
  // When Access Type in premodified outbound settings value was 1 that means that the initial access was allowed. When Access Type in premodified outbound settings value was 2 that means that the initial access was blocked.
  // When Access Type in modified outbound settings value is 1 that means that now access is allowed. When Access Type in modified outbound settings value is 2 that means that now access is blocked.
  AuditLogs
  | where OperationName has "Update a partner cross-tenant access setting"
  | mv-apply TargetResource = TargetResources on
    (
        where TargetResource.type =~ "Policy"
        | extend Properties = TargetResource.modifiedProperties
    )
  | mv-apply Property = Properties on
    (
        where Property.displayName =~ "b2bCollaborationOutbound"
        | extend PremodifiedOutboundSettings = trim('"',tostring(Property.oldValue)),
                 ModifiedOutboundSettings = trim(@'"',tostring(Property.newValue))
    )
  | where PremodifiedOutboundSettings != ModifiedOutboundSettings
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has "Update a partner cross-tenant access setting"

Stage 3: kusto:mv-apply

| mv-apply TargetResource = TargetResources on
  (
      where TargetResource.type =~ "Policy"
      | extend Properties = TargetResource.modifiedProperties
  )

Stage 4: kusto:mv-apply

| mv-apply Property = Properties on
  (
      where Property.displayName =~ "b2bCollaborationOutbound"
      | extend PremodifiedOutboundSettings = trim('"',tostring(Property.oldValue)),
               ModifiedOutboundSettings = trim(@'"',tostring(Property.newValue))
  )

Stage 5: where

| where PremodifiedOutboundSettings != ModifiedOutboundSettings

Stage 6: extend (6 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNamematch
  • Update a partner cross-tenant access setting transforms: term corpus 4 (kusto 4)
field:"OperationName" kind:match value:"Update a partner cross-tenant access setting"
Property.displayNameeq
  • b2bCollaborationOutbound transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"b2bCollaborationOutbound"
TargetResource.typeeq
  • Policy transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"Policy"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

Cynerio - IoT - Default password

#
Severity
high
Time window
5h
Source
github.com/Azure/Azure-Sentinel

User signed in using default credentials

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 84e0ea1f-766d-4775-836a-c0c9cca05085
name: Cynerio - IoT - Default password
description: User signed in using default credentials
severity: High
tags: 
  - Schema: ASimAuthentication
    SchemaVersion: 0.1.3
requiredDataConnectors:
  - connectorId: CynerioSecurityEvents
    dataTypes:
      - CynerioEvent_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1552
query: |
  CynerioEvent_CL
  | where TimeGenerated > ago(24h)
  | where module_s == 'IDS'
  | where title_s == 'Default Password'
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: asset_ip_s
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails: 
  Service: service_s
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CynerioEvent_CL

Stage 2: where

| where TimeGenerated > ago(24h)

Stage 3: where

| where module_s == 'IDS'

Stage 4: where

| where title_s == 'Default Password'

Indicators

These rows show field, operator, and value matches.

Dataverse - TI map URL to DataverseActivity

#
Status
available
Severity
medium
Time window
14d
Group by
IndicatorId, Url
Source
github.com/Azure/Azure-Sentinel

Identifies a match in DataverseActivity from any URL IOC from Microsoft Sentinel Threat Intelligence.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: d88a0e22-3b6a-40c2-af28-c064b44d03b7
kind: Scheduled
name: Dataverse - TI map URL to DataverseActivity
description: Identifies a match in DataverseActivity from any URL IOC from Microsoft
  Sentinel Threat Intelligence.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - Persistence
relevantTechniques:
  - T1566
  - T1456
  - T1474
  - T0819
  - T0865
  - T0862
  - T0863
  - T1204
  - T1574
  - T0873
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true
  | where isnotempty(Url)
  | join kind=innerunique (
      DataverseActivity
      | where TimeGenerated >= ago(dt_lookBack)
      | where Message in ("Create", "Update")
      | where isnotempty(Fields) and Fields has "http"
      | extend
          ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
          DataverseActivity_TimeGenerated = TimeGenerated
      | mv-expand Url = ExtractedUrls
      | project
          DataverseActivity_TimeGenerated,
          tostring(Url),
          UserId,
          ClientIp,
          InstanceUrl,
          EntityName
      )
      on Url
  | where DataverseActivity_TimeGenerated < ExpirationDateTime
  | summarize DataverseActivity_TimeGenerated  = arg_max(DataverseActivity_TimeGenerated, *) by IndicatorId, Url
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      DataverseActivity_TimeGenerated,
      Description,
      ActivityGroupNames,
      IndicatorId,
      ThreatType,
      ExpirationDateTime,
      ConfidenceScore,
      UserId,
      ClientIp,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix,
      Url
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - TI match on URL in {{InstanceUrl}}
  alertDescriptionFormat: Malicous IP {{Url}} was found in {{InstanceUrl}}. Associated
    user is {{UserId}}
version: 3.2.0

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack) and ExpirationDateTime > now()

Stage 3: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 4: where

| where Active == true

Stage 5: where

| where isnotempty(Url)

Stage 6: join

| join kind=innerunique (
    DataverseActivity
    | where TimeGenerated >= ago(dt_lookBack)
    | where Message in ("Create", "Update")
    | where isnotempty(Fields) and Fields has "http"
    | extend
        ExtractedUrls = extract_all("(http[s]?://(?:[a-zA-Z\\.-]|[0-9])+)", tostring(Fields)),
        DataverseActivity_TimeGenerated = TimeGenerated
    | mv-expand Url = ExtractedUrls
    | project
        DataverseActivity_TimeGenerated,
        tostring(Url),
        UserId,
        ClientIp,
        InstanceUrl,
        EntityName
    )
    on Url

Stage 7: where

where DataverseActivity_TimeGenerated < ExpirationDateTime

Stage 8: summarize

summarize DataverseActivity_TimeGenerated by IndicatorId, Url

Stage 9: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 10: project

project AccountName, ActivityGroupNames, ClientIp, CloudAppId, ConfidenceScore, DataverseActivity_TimeGenerated, Description, ExpirationDateTime, IndicatorId, InstanceUrl, ThreatType, UPNSuffix, Url, UserId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true corpus 60 (kusto 60)
field:"Active" kind:eq value:"true"
Fieldsis_not_null
  • (no value, null check)
field:"Fields" kind:is_not_null
Fieldsmatch
  • http transforms: term
field:"Fields" kind:match value:"http"
Messagein
  • Create
  • Update
field:"Message" kind:in
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ActivityGroupNamesproject
ClientIpproject
CloudAppIdproject
ConfidenceScoreproject
DataverseActivity_TimeGeneratedproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
InstanceUrlproject
ThreatTypeproject
UPNSuffixproject
Urlproject
UserIdproject

Detect URLs containing known malicious keywords or commands (ASIM Web Session)

#
Status
available
Severity
high
Time window
5m
Group by
Decoded_url, DstIpAddr, SrcHostname, SrcIpAddr, SrcUsername, Url
Source
github.com/Azure/Azure-Sentinel

'The utilization of system commands or functions in the request URL may suggest that an attacker is trying to gain unauthorized access to the environment by exploiting a vulnerable service.'

MITRE ATT&CK Coverage

Rule Body

id: 32c08696-2e37-4730-86f8-97d9c8b184c9
name: Detect URLs containing known malicious keywords or commands (ASIM Web Session)
description: |
  'The utilization of system commands or functions in the request URL may suggest that an attacker is trying to gain unauthorized access to the environment by exploiting a vulnerable service.'
severity: High
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1133
  - T1071
query: |
  let lookback = 5m;
  let RiskyCommandsInUrl = materialize(externaldata(Commands: string)
      [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/CommandsInURL.csv"]
      with(format="csv", ignoreFirstRecord=True));
  let CustomRiskyCommandsInUrl = (_ASIM_GetWatchlistRaw("Web_RiskyCommandsInUrl") // Create new Watchlist and add your custom indicators(Optional)
      | extend
          Commands = tostring(WatchlistItem["Commands"])
      | project Commands
      | where isnotempty(Commands));
  let CombinedRiskyCommandsInUrl = union RiskyCommandsInUrl, CustomRiskyCommandsInUrl;
  let knownRiskyCommandsInUrl=toscalar(CombinedRiskyCommandsInUrl
      | where isnotempty(Commands)
      | summarize make_set(Commands, 1000));
  // You can add more keywords to the query as necessary, depending on the specific indicators you want to detect.
  _Im_WebSession (starttime=ago(lookback), eventresult='Success', url_has_any=knownRiskyCommandsInUrl)
  | where isnotempty(Url)
  | project Url, SrcIpAddr, SrcUsername, SrcHostname, DstIpAddr, TimeGenerated
  | extend Decoded_url = url_decode(Url)
  | where Decoded_url has_any (knownRiskyCommandsInUrl)
  | summarize
      EventCount=count(),
      EventStartTime=min(TimeGenerated),
      EventEndTime=max(TimeGenerated)
      by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url, DstIpAddr
  | extend
      Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
      UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DstIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUsername
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventCount: EventCount
  Decoded_url: Decoded_url
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
alertDetailsOverride:
  alertDisplayNameFormat: "User '{{SrcUsername}}' with IP '{{SrcIpAddr}}' has been identified as making request for URL '{{Url}}' that includes a recognizable malicious command"
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 5m;
let CombinedRiskyCommandsInUrl = union RiskyCommandsInUrl, CustomRiskyCommandsInUrl;

Let Binding: RiskyCommandsInUrl

let RiskyCommandsInUrl = materialize(externaldata(Commands: string)
    [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/CommandsInURL.csv"]
    with(format="csv", ignoreFirstRecord=True));

Let Binding: CustomRiskyCommandsInUrl

let CustomRiskyCommandsInUrl = (_ASIM_GetWatchlistRaw("Web_RiskyCommandsInUrl")
    | extend
        Commands = tostring(WatchlistItem["Commands"])
    | project Commands
    | where isnotempty(Commands));

Let Binding: knownRiskyCommandsInUrl used in Stages 1, 5

let knownRiskyCommandsInUrl = toscalar(CombinedRiskyCommandsInUrl
    | where isnotempty(Commands)
    | summarize make_set(Commands, 1000));

Stage 1: source

_Im_WebSession (starttime=ago(lookback), eventresult='Success', url_has_any=knownRiskyCommandsInUrl)

Stage 2: where

| where isnotempty(Url)

Stage 3: project

| project Url, SrcIpAddr, SrcUsername, SrcHostname, DstIpAddr, TimeGenerated

Stage 4: extend

| extend Decoded_url = url_decode(Url)

Stage 5: where

| where Decoded_url has_any (knownRiskyCommandsInUrl)

Stage 6: summarize

| summarize
    EventCount=count(),
    EventStartTime=min(TimeGenerated),
    EventEndTime=max(TimeGenerated)
    by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url, DstIpAddr

Stage 7: extend

| extend
    Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
    UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Decoded_urlmatch
  • knownRiskyCommandsInUrl transforms: term
field:"Decoded_url" kind:match value:"knownRiskyCommandsInUrl"
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Decoded_urlsummarize
DstIpAddrsummarize
EventCountsummarize
EventEndTimesummarize
EventStartTimesummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
Urlsummarize
Nameextend
UPNSuffixextend

GitHub - Repository was created

#
Status
available
Severity
medium
Time window
7d
Source
github.com/Azure/Azure-Sentinel

Detect activities when a repository was created. This query runs every day and its severity is Medium.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 0b85a077-8ba5-4cb5-90f7-1e882afe10c2
name: GitHub - Repository was created
description: |
  'Detect activities when a repository was created. This query runs every day and its severity is Medium.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
    GitHubAuditData
    | where Action == "repo.create"
    | extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

GitHubAuditData

Stage 2: where

| where Action == "repo.create"

Stage 3: extend

| extend AccountName = tostring(split(Actor, "@")[0]), AccountUPNSuffix = tostring(split(Actor, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • repo.create
field:"Action" kind:eq value:"repo.create"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameextend
AccountUPNSuffixextend

Mimecast Data Leak Prevention - Hold

#
Severity
informational
Time window
15m
Source
github.com/Azure/Azure-Sentinel

Detects threat for data leak when action is hold

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 8e52bcf1-4f50-4c39-8678-d9efad64e379
name: Mimecast Data Leak Prevention - Hold
description: Detects threat for data leak when action is hold
severity: Informational
requiredDataConnectors:
  - connectorId: MimecastSEGAPI
    dataTypes:
      - MimecastDLP
enabled: true
query: |
  MimecastDLP 
  | where Action == "hold"
  | extend SenderAddress = ['Sender Address'] ,RecipientAddress = ['Recipient Address']
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- Exfiltration
relevantTechniques:
- T1030
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: MailMessage
  fieldMappings:
  - identifier: Sender
    columnName: SenderAddress
  - identifier: Recipient
    columnName: RecipientAddress
  - identifier: DeliveryAction
    columnName: Action
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastDLP

Stage 2: where

| where Action == "hold"

Stage 3: extend

| extend SenderAddress = ['Sender Address'] ,RecipientAddress = ['Recipient Address']

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
RecipientAddressextend
SenderAddressextend

PaloAlto - Inbound connection to high risk ports

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

'Detects inbound connection to high risk ports.'

MITRE ATT&CK Coverage

Rule Body

id: b2dd2dac-51c9-11ec-bf63-0242ac130002
name: PaloAlto - Inbound connection to high risk ports
description: |
  'Detects inbound connection to high risk ports.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let HighRiskPorts = dynamic(['21', '22', '23', '25', '53', '443', '110', '135', '137', '138', '139', '1433', '1434']);
  PaloAltoCDLEvent
  | where EventResourceId =~ 'TRAFFIC'
  | where ipv4_is_private(SrcIpAddr) == false
  | where DstPortNumber in (HighRiskPorts)
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let HighRiskPorts = dynamic(['21', '22', '23', '25', '53', '443', '110', '135', '137', '138', '139', '1433', '1434']);

Stage 1: source

PaloAltoCDLEvent

Stage 2: where

| where EventResourceId =~ 'TRAFFIC'

Stage 3: where

| where ipv4_is_private(SrcIpAddr) == false

Stage 4: where

| where DstPortNumber in (HighRiskPorts)

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:SrcIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Pathlock TDnR - SAP Cloud Account Administration Events

#
Status
available
Severity
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

Detects account administration events in SAP Cloud environments, forwarded by Pathlock Threat Detection and Response. Suspicious cloud account activities may indicate unauthorized provisioning, privilege escalation, or account takeover in SAP cloud tenants.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c21
name: Pathlock TDnR - SAP Cloud Account Administration Events
kind: Scheduled
description: >-
  Detects account administration events in SAP Cloud environments, forwarded by Pathlock Threat
  Detection and Response. Suspicious cloud account activities may indicate unauthorized provisioning,
  privilege escalation, or account takeover in SAP cloud tenants.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1136
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CLOUD_ACCOUNT_LOGS"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "CLOUD_ACCOUNT_LOGS"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Privileged Role Assigned Outside PIM

#
Status
available
Severity
low
Time window
1d
Source
github.com/Azure/Azure-Sentinel

Identifies a privileged role being assigned to a user outside of PIM Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

PlatformRecord / event type
AzureAdd member to role

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 269435e3-1db8-4423-9dfc-9bf59997da1c
name: Privileged Role Assigned Outside PIM
description: |
  'Identifies a privileged role being assigned to a user outside of PIM
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor-1'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078.004
tags:
    - AADSecOpsGuide
query: |
  AuditLogs
  | where Category =~ "RoleManagement"
  | where OperationName has "Add member to role outside of PIM"
          or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
    )
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName    
      - identifier: Name
        columnName: InitiatedByName      
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserPrincipalName   
      - identifier: Name
        columnName: TargetName      
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.6
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "RoleManagement"

Stage 3: where

| where OperationName has "Add member to role outside of PIM"
        or (LoggedByService =~ "Core Directory" and OperationName =~ "Add member to role" and Identity != "MS-PIM" and Identity != "MS-PIM-Fairfax")

Stage 4: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend TargetUserPrincipalName = tostring(TargetResource.userPrincipalName)
  )

Stage 5: extend (7 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend TargetName = tostring(split(TargetUserPrincipalName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserPrincipalName,'@',1)[0])
| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
Identityne
  • MS-PIM corpus 3 (kusto 3)
  • MS-PIM-Fairfax corpus 3 (kusto 3)
field:"Identity" kind:ne
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
field:"azure_ad::logged_by_service" kind:eq value:"Core Directory"
OperationNameeq
  • Add member to role corpus 2 (kusto 2)
field:"OperationName" kind:eq value:"Add member to role"
OperationNamematch
  • Add member to role outside of PIM transforms: term corpus 2 (kusto 2)
field:"OperationName" kind:match value:"Add member to role outside of PIM"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
TargetNameextend
TargetUPNSuffixextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

PROD (TM010.1) - BITLOCKER - Stored Bitlocker Recovery Key to Tier Level Computer Object

#
Status
available
Severity
informational
Time window
5h
Source
github.com/Azure/Azure-Sentinel

A Bitlocker recovery key was stored within a Tier Model computer object.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: eea9c640-afe0-4ce4-9a41-c409d4c2c924
name: 'PROD (TM010.1) - BITLOCKER - Stored Bitlocker Recovery Key to Tier Level Computer Object'
description: |
  A Bitlocker recovery key was stored within a Tier Model computer object.
severity: Informational
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1552.004
query: |
  SecurityEvent
  | where EventID == 5137
      and (EventData matches regex @"(?i)OU=Domain Controllers"
          or EventData matches regex @"(?i)OU=Tier 0 PAW"
          or EventData matches regex @"(?i)OU=Tier 1 PAW"
          or EventData matches regex @"(?i)OU=Tier 2 PAW"
          or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 1 Member Servers")
  | extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
  | where ObjectClass has "msFVE-RecoveryInformation"
  | extend ObjectBitName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
  | extend ObjectFullName = extract("(?i)(CN=[^,]+,(?i)OU=[^,]+)", 1, ObjectBitName_e)
  | extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectFullName)
  | extend ObjectTier = case(
      ObjectFullName has "Tier 0", "Tier 0",
      ObjectFullName has "Tier 1", "Tier 1",
      ObjectFullName has "Tier Model Administration", "Tier 0",
      ObjectFullName has "Tier Model Computer Quarantine", "Tier 0",
      ObjectFullName has "Domain Controllers", "Tier 0",
      "Unknown")
  | extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
  | extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
  | project TimeGenerated
     , ObjectName
     , ObjectTier
     , ObjectClass
     , ActivityType
     , Account
     , Domain
     , Computer
     , Channel
     , EventID
     , EventData
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ObjectName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
customDetails:
  ObjectName: ObjectName
  ObjectTier: ObjectTier
  ObjectClass: ObjectClass
  ActivityType: ActivityType
  Account: Account
  Domain: Domain
  Computer: Computer
  Channel: Channel
  EventID: EventID
  EventData: EventData
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: '(TM010.1) A {{ObjectTier}} Bitlocker Recovery Key was stored in AD'
  alertDescriptionFormat: 'A Tier Model object Bitlocker Key was stored in AD. This Informational alert is generated as part of reporting.'
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 5137
    and (EventData matches regex @"(?i)OU=Domain Controllers"
        or EventData matches regex @"(?i)OU=Tier 0 PAW"
        or EventData matches regex @"(?i)OU=Tier 1 PAW"
        or EventData matches regex @"(?i)OU=Tier 2 PAW"
        or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 1 Member Servers")

Stage 3: extend

| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)

Stage 4: where

| where ObjectClass has "msFVE-RecoveryInformation"

Stage 5: extend (6 consecutive steps)

| extend ObjectBitName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend ObjectFullName = extract("(?i)(CN=[^,]+,(?i)OU=[^,]+)", 1, ObjectBitName_e)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectFullName)
| extend ObjectTier = case(
    ObjectFullName has "Tier 0", "Tier 0",
    ObjectFullName has "Tier 1", "Tier 1",
    ObjectFullName has "Tier Model Administration", "Tier 0",
    ObjectFullName has "Tier Model Computer Quarantine", "Tier 0",
    ObjectFullName has "Domain Controllers", "Tier 0",
    "Unknown")
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)

Stage 6: project

| project TimeGenerated
   , ObjectName
   , ObjectTier
   , ObjectClass
   , ActivityType
   , Account
   , Domain
   , Computer
   , Channel
   , EventID
   , EventData

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDataregex_match
  • (?i)OU=Domain Controllers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 1 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 1 PAW corpus 6 (kusto 6)
  • (?i)OU=Tier 2 PAW corpus 6 (kusto 6)
field:"EventData" kind:regex_match
EventIDeq
  • 5137 corpus 8 (kusto 5, elastic 3)
field:"EventID" kind:eq value:"5137"
ObjectClassmatch
  • msFVE-RecoveryInformation transforms: term
field:"ObjectClass" kind:match value:"msFVE-RecoveryInformation"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
ObjectClassproject
ObjectNameproject
ObjectTierproject
TimeGeneratedproject

SlackAudit - Unknown User Agent

#
Status
available
Severity
low
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'Detects Slack workspace activity from unknown user agents by comparing recent UserAgentOriginal values against a 14d baseline of known user agents.'

MITRE ATT&CK Coverage

Rule Body

id: 3b11f06e-4afd-4ae6-8477-c61136619ac8
name: SlackAudit - Unknown User Agent
description: |
  'Detects Slack workspace activity from unknown user agents by comparing recent UserAgentOriginal values against
  a 14d baseline of known user agents.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: SlackAuditAPI
    dataTypes:
      - SlackAudit_CL
queryFrequency: 24h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics: 
  - CommandAndControl
relevantTechniques:
  - T1071
  - T1071.001
query: |
  let lbperiod = 14d;
  let known_UAs = SlackAudit
  | where TimeGenerated > ago(lbperiod)
  | where isnotempty(UserAgentOriginal)
  | summarize makeset(UserAgentOriginal);
  SlackAudit
  | where UserAgentOriginal !in (known_UAs)
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
alertDetailsOverride:
  alertDisplayNameFormat: Slack audit unknown user agent for {{AccountCustomEntity}}
  alertDescriptionFormat: Slack activity from {{AccountCustomEntity}} used unknown user agent {{UserAgentOriginal}}
customDetails:
  Account: AccountCustomEntity
  UserAgent: UserAgentOriginal
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lbperiod = 14d;

Let Binding: known_UAs used in Stage 2

let known_UAs = SlackAudit
| where TimeGenerated > ago(lbperiod)
| where isnotempty(UserAgentOriginal)
| summarize makeset(UserAgentOriginal);

Stage 1: source

SlackAudit

Stage 2: where

| where UserAgentOriginal !in (known_UAs)

Stage 3: extend

| extend AccountCustomEntity = SrcUserName

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
UserAgentOriginaleqknown_UAsexcludes:UserAgentOriginal field:"UserAgentOriginal" value:"known_UAs"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Suspicious Network Beacons - Microsoft Defender(MDE/M365D)

#
Group by
DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query detects suspicious beaconing activity by analyzing DeviceNetworkEvents data.

MITRE ATT&CK Coverage

TacticTechniques
Command & ControlNo specific technique

Telemetry Coverage

Rule Body

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-machine-learning-and-277c4c30304f
// Part-2: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-ml-and-kql-part-2-bff46cfc1e7e
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d. Refactoring and additional analysis are required. 
// WARNING!: Since MDE doesn't log every single network connection, there is a chance of FALSE NEGATIVES. 
//
// Query parameters:
let starttime = 1d;
let endtime = 1s;
// Set the minimum beacon sleep. Increase it to get less results. the format is (hour,minute,second.milisecond).
// Be careful when changing the value. run " print ['timespan'] = make_timespan(0, x, y) " to verify you have the correct value set. 
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);  
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax=toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50; // jitter in percentage. Set to filter out false positives: small threshold means tighter filtering/fewer results.
// Outlier thresholds. 1.5 means the value is a normal outlier, 3 means the value is far far out.
let OutlierThresholdMax = 2; //increase or decrease this value to get more or less results
// Time delta data set can have some outliers. Define how many outliers are acceptable for a beacon. Values between 1 to 3 should be fine.
let OutlierCountMax = 3; // increasing the value provides more results.
// Define how many devices can have the same beacon. 
let CompromisedDeviceCountMax = 10; // increasing the value provides more results. 
// Get all beacon candidates just by jitter filtering.
let BeaconCandidates = materialize (
    DeviceNetworkEvents
    | where Timestamp between (ago(starttime)..ago(endtime))
    | where RemoteIPType !in ("Reserved", "Private", "LinkLocal", "Loopback")
    | where isnotempty(RemoteIP) and RemoteIP !in ("0.0.0.0") 
    | where not (ipv4_is_private(RemoteIP))
    | where ActionType in ("ConnectionSuccess", "CsonnectionRequest", "CsonnectionFailed") // Fix the typos if you want to inlcude connreq. and connfail. 
    | summarize hint.strategy=shuffle make_set(Timestamp) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
    | where array_length(set_Timestamp) > TotalEventsThresholdMin and array_length(set_Timestamp) < TotalEventsThresholdMax
    | project DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort, Timestamp=array_sort_asc(set_Timestamp)
    | mv-apply Timestamp to typeof(datetime) on 
    (     
        extend nextTimestamp = next(Timestamp, 1), nextInitiatingProcessAccountName = next(InitiatingProcessAccountName, 1), nextDeviceId = next(DeviceId, 1), nextDeviceName = next(DeviceName, 1), nextRemoteIP = next(RemoteIP, 1), nextRemotePort = next(RemotePort, 1), nextInitiatingProcessFileName = next(InitiatingProcessFileName, 1)
        | extend TimeDeltaInSeconds = datetime_diff('second',nextTimestamp,Timestamp)
        | where nextInitiatingProcessAccountName == InitiatingProcessAccountName and nextDeviceId == DeviceId and nextDeviceName == DeviceName and nextInitiatingProcessFileName == InitiatingProcessFileName and nextRemoteIP == RemoteIP and nextRemotePort == RemotePort
        | project Timestamp, TimeDeltaInSeconds, DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort
        // compute statistical values including standard deviation
        | summarize count(), min(Timestamp), max(Timestamp), Duration=datetime_diff("second", max(Timestamp), min(Timestamp)), 
            percentiles(TimeDeltaInSeconds, 5, 25, 50, 75, 95), 
            TimeDeltaList=make_list(TimeDeltaInSeconds) by DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort
        | extend (TimeDeltaInSeconds_min,TimeDeltaInSeconds_min_index,TimeDeltaInSeconds_max,TimeDeltaInSeconds_max_index,TimeDeltaInSeconds_avg,TimeDeltaInSeconds_stdev,TimeDeltaInSeconds_variance)=series_stats(TimeDeltaList)
        | extend Jitter=(TimeDeltaInSeconds_stdev/TimeDeltaInSeconds_avg)*100,
                 BeaconSleepMin=TimeDeltaInSeconds_avg - TimeDeltaInSeconds_stdev,
                 BeaconSleepMax=TimeDeltaInSeconds_avg + TimeDeltaInSeconds_stdev
        // Filter out impossible beacons based on jitter threshold defined.
        | where Jitter < JitterThreshold
    )
    // Try to enricht IP with the hostname
    | join kind=leftouter
        (
        DeviceNetworkEvents
        | where Timestamp > ago(starttime+1d)
        // Extract domain from the RemoteUrl
        | extend Host=tostring(parse_url(iif(RemoteUrl !startswith "http", strcat(@'http://',RemoteUrl),RemoteUrl)).Host)
        | extend domain = reverse(replace(@'([A-z0-9-]+\.[A-z0-9-]+\.[A-z0-9-]+)\..*',@'\1',reverse(Host)))
        | project domain, RemoteIP
        | summarize Domains = make_set(domain) by RemoteIP
        ) on RemoteIP
        | project-reorder DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain
)
;
// Get potentially suspicious beacons based on CompromisedDeviceCountMax
let PotentialBeacons = materialize (
    BeaconCandidates
    | summarize dcount(DeviceId) by InitiatingProcessFileName, RemoteIP, RemotePort
    // Filter out beacon destinations if many devices are connecting to it (like windows update)
    | where dcount_DeviceId <= CompromisedDeviceCountMax
    | join kind=inner BeaconCandidates on InitiatingProcessFileName, RemoteIP, RemotePort
    | project-away *1
    )
    ;
// Get candidates that can't be beacons based on outlier analysis on the time delta
let ImpossibleBeaconsByTimeDelta = materialize 
    (
    PotentialBeacons
    | extend outliers = series_outliers(TimeDeltaList)
    | mv-expand TimeDeltaList, outliers to typeof(double)
    | where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax) // outlier can be negative or positive.
    | summarize count(), make_set(outliers) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
    | where count_ > OutlierCountMax
    )
    ;
// Remove ImpossibleBeaconsByTimeDelta from potentially suspicious beacons. 
PotentialBeacons
| join kind=leftantisemi ImpossibleBeaconsByTimeDelta on DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
| extend Timestamp = min_Timestamp // just to make it easy to jump to the device timeline etc. 
// if the logs have extra information, they can be used for filtering the nonmalicious destinations
| order by Jitter asc, TimeDeltaInSeconds_avg asc

Stages and Predicates

Parameters

let starttime = 1d;
let endtime = 1s;
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax = toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50;
let OutlierThresholdMax = 2;
let OutlierCountMax = 3;
let CompromisedDeviceCountMax = 10;

let BeaconCandidates and let PotentialBeacons are inlined into the numbered stages below.

Let Binding: ImpossibleBeaconsByTimeDelta used in Stage 19

let ImpossibleBeaconsByTimeDelta = materialize 
    (
    PotentialBeacons
    | extend outliers = series_outliers(TimeDeltaList)
    | mv-expand TimeDeltaList, outliers to typeof(double)
    | where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax)
    | summarize count(), make_set(outliers) by DeviceId, DeviceName,InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort
    | where count_ > OutlierCountMax
    );

Stage 1: source

let BeaconCandidates

Stage 2: source

let PotentialBeacons

Stage 3: source

DeviceNetworkEvents

Stage 4: where

where Timestamp between (ago(86400s) .. ago(1s))

Stage 5: where

where not (RemoteIPType in ("LinkLocal", "Loopback", "Private", "Reserved"))

Stage 6: where

where not (RemoteIP == "0.0.0.0") and isnotempty(RemoteIP)

Stage 7: where

where not ((ipv4_is_in_range(RemoteIP, "10.0.0.0/8") or ipv4_is_in_range(RemoteIP, "172.16.0.0/12") or ipv4_is_in_range(RemoteIP, "192.168.0.0/16") or ipv4_is_in_range(RemoteIP, "169.254.0.0/16") or ipv4_is_in_range(RemoteIP, "127.0.0.0/8")))

Stage 8: where

where ActionType in ("ConnectionSuccess", "CsonnectionFailed", "CsonnectionRequest")

Stage 9: summarize

summarize by DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort

Stage 10: where

where <unknown:array_length(, set_Timestamp)> < TotalEventsThresholdMax and set_Timestamp > 15

Stage 11: project

project DeviceId, DeviceName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessFileName, RemoteIP, RemotePort, Timestamp

Stage 12: kusto:mv-apply

kusto:mv-apply Jitter < JitterThreshold and nextDeviceId =~ DeviceId and nextDeviceName =~ DeviceName and nextInitiatingProcessAccountName =~ InitiatingProcessAccountName and nextInitiatingProcessFileName =~ InitiatingProcessFileName and nextRemoteIP =~ RemoteIP and nextRemotePort =~ RemotePort

Stage 13: join

join kind=leftouter (DeviceNetworkEvents) on RemoteIP

Stage 14: project-reorder

project-reorder

Stage 15: summarize

summarize by InitiatingProcessFileName, RemoteIP, RemotePort

Stage 16: where

where dcount_DeviceId <= 10

Stage 17: join

join kind=inner (BeaconCandidates) on InitiatingProcessFileName, RemoteIP, RemotePort

Stage 18: project-away

project-away *1

Stage 19: join (negated)

join kind=leftantisemi (ImpossibleBeaconsByTimeDelta) on DeviceId, DeviceName, InitiatingProcessAccountName, InitiatingProcessAccountDomain, InitiatingProcessFileName, RemoteIP, RemotePort

Stage 20: extend

extend Timestamp

Stage 21: sort

sort by Jitter, TimeDeltaInSeconds_avg

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypein
  • ConnectionSuccess
  • CsonnectionFailed
  • CsonnectionRequest
field:"ActionType" kind:in
RemoteIPcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"RemoteIP" kind:cidr_match
RemoteIPin
  • 0.0.0.0
field:"RemoteIP" kind:in value:"0.0.0.0"
RemoteIPis_not_null
  • (no value, null check)
field:"RemoteIP" kind:is_not_null
RemoteIPTypein
  • LinkLocal
  • Loopback
  • Private
  • Reserved
field:"RemoteIPType" kind:in
dcount_DeviceIdle
  • 10 transforms: number
field:"dcount_DeviceId" kind:le value:"10"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RemoteIPTypeinLinkLocal, Loopback, Private, Reservedexcludes:RemoteIPType
RemoteIPeq0.0.0.0excludes:RemoteIP field:"RemoteIP" value:"0.0.0.0"
RemoteIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:RemoteIP
outliersgt2excludes:outliers field:"outliers" value:"2"
outlierslt-3excludes:outliers field:"outliers" value:"-3"
ActionTypeinConnectionSuccess, CsonnectionFailed, CsonnectionRequestexcludes:ActionType field:"ActionType" value:"ConnectionSuccess" field:"ActionType" value:"CsonnectionFailed" field:"ActionType" value:"CsonnectionRequest"
Jittercross_field_compareJitterThresholdexcludes:Jitter field:"Jitter" value:"JitterThreshold"
RemoteIPis_not_null(no value, null check)excludes:RemoteIP
count_gt3excludes:count_ field:"count_" value:"3"
dcount_DeviceIdle10excludes:dcount_DeviceId field:"dcount_DeviceId" value:"10"
nextDeviceIdcross_field_compareDeviceIdexcludes:nextDeviceId field:"nextDeviceId" value:"DeviceId"
nextDeviceNamecross_field_compareDeviceNameexcludes:nextDeviceName field:"nextDeviceName" value:"DeviceName"
nextInitiatingProcessAccountNamecross_field_compareInitiatingProcessAccountNameexcludes:nextInitiatingProcessAccountName field:"nextInitiatingProcessAccountName" value:"InitiatingProcessAccountName"
nextInitiatingProcessFileNamecross_field_compareInitiatingProcessFileNameexcludes:nextInitiatingProcessFileName field:"nextInitiatingProcessFileName" value:"InitiatingProcessFileName"
nextRemoteIPcross_field_compareRemoteIPexcludes:nextRemoteIP field:"nextRemoteIP" value:"RemoteIP"
nextRemotePortcross_field_compareRemotePortexcludes:nextRemotePort field:"nextRemotePort" value:"RemotePort"
set_Timestampcross_field_compareTotalEventsThresholdMaxexcludes:set_Timestamp field:"set_Timestamp" value:"TotalEventsThresholdMax"
set_Timestampgt15excludes:set_Timestamp field:"set_Timestamp" value:"15"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingProcessFileNamesummarize
RemoteIPsummarize
RemotePortsummarize
Timestampextend

The download of potentially risky files from the Discord Content Delivery Network (CDN) (ASIM Web Session)

#
Status
available
Severity
medium
Time window
1d
Group by
DiscordServerId, EventEndTime, EventStartTime, SrcHostname, SrcIpAddr, SrcUsername, Url
Source
github.com/Azure/Azure-Sentinel

'This detection mechanism identifies instances where requests are made to Discord CDN addresses for file extensions that are considered risky. It triggers when a callout is made to a Discord server that has only been encountered once in your environment. The uniqueness of Discord servers is determined based on the server ID present in the request URL (DiscordServerId in the query). Discord CDN has been utilized in numerous campaigns to download additional payloads, highlighting the importance of monitoring such activities. The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'

MITRE ATT&CK Coverage

Rule Body

id: b7fe8f27-7010-404b-aec5-6e5245cea580
name: The download of potentially risky files from the Discord Content Delivery Network (CDN) (ASIM Web Session)
description: |
  'This detection mechanism identifies instances where requests are made to Discord CDN addresses for file extensions that are considered risky.
    It triggers when a callout is made to a Discord server that has only been encountered once in your environment. The uniqueness of Discord servers is determined based on the server ID present in the request URL (DiscordServerId in the query).
    Discord CDN has been utilized in numerous campaigns to download additional payloads, highlighting the importance of monitoring such activities.
    The query includes a sample set of popular web script extensions (scriptExtensions), which should be customized to align with the specific requirements of your environment'
severity: Medium
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071.001
query: |
  let lookback = 1d;
  let connectionThreshold = 1;
  let RiskyFileExtensions = materialize(externaldata(Extensions: string)
      [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/RiskyFileExtensionsInUrl.csv"]
      with(format="csv", ignoreFirstRecord=True));
  let CustomRiskyFileExtensions = (_ASIM_GetWatchlistRaw("Web_RiskyFileExtensions") // Create new Watchlist and add your custom indicators(Optional)
      | extend
          Extensions = tostring(WatchlistItem["Extensions"])
      | project Extensions
      | where isnotempty(Extensions));
  let CombinedRiskyFileExtensions = union RiskyFileExtensions, CustomRiskyFileExtensions;
  let knownRiskyFileExtensions=toscalar(CombinedRiskyFileExtensions
      | where isnotempty(Extensions)
      | summarize make_set(Extensions, 1000));
  let discord=dynamic(["cdn.discordapp.com", "media.discordapp.com"]);
  let WebData = _Im_WebSession(starttime=ago(lookback), url_has_any=discord, eventresult='Success')
      | where isnotempty(Url)
      | project Url, SrcUsername, SrcIpAddr, TimeGenerated, SrcHostname
      | where Url has "attachments"
      | extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url);
  WebData
  | summarize
      dcount(Url),
      min(TimeGenerated),
      max(TimeGenerated)
      by DiscordServerId
  | join kind=inner (WebData) on DiscordServerId
  | where dcount_Url <= connectionThreshold and Url has_any (knownRiskyFileExtensions)
  | summarize EventCount = count()
      by
      EventStartTime=min_TimeGenerated,
      EventEndTime=max_TimeGenerated,
      SrcUsername,
      SrcHostname,
      SrcIpAddr,
      Url
  | extend
      Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
      UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
alertDetailsOverride:
  alertDisplayNameFormat: "User '{{SrcUsername}}' with the IP address '{{SrcIpAddr}}' has been detected downloading potentially risky files from the Discord CDN"
  alertDescriptionFormat: " Client requested for URL '{{Url}}' that contains a files hosted on a recognized Discord Content Delivery Network (CDN) which are considered to be potentially risky. It is essential to investigate further to determine the nature of the files being requested and the intent of the users involved"
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1d;
let connectionThreshold = 1;
let CombinedRiskyFileExtensions = union RiskyFileExtensions, CustomRiskyFileExtensions;
let discord = dynamic(["cdn.discordapp.com", "media.discordapp.com"]);

let WebData is inlined into the numbered stages below.

Let Binding: RiskyFileExtensions

let RiskyFileExtensions = materialize(externaldata(Extensions: string)
    [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/RiskyFileExtensionsInUrl.csv"]
    with(format="csv", ignoreFirstRecord=True));

Let Binding: CustomRiskyFileExtensions

let CustomRiskyFileExtensions = (_ASIM_GetWatchlistRaw("Web_RiskyFileExtensions")
    | extend
        Extensions = tostring(WatchlistItem["Extensions"])
    | project Extensions
    | where isnotempty(Extensions));

Let Binding: knownRiskyFileExtensions used in Stage 8

let knownRiskyFileExtensions = toscalar(CombinedRiskyFileExtensions
    | where isnotempty(Extensions)
    | summarize make_set(Extensions, 1000));

Stages 1 to 5 define let WebData as the rule's main pipeline source. Stages 6 to 10 use it.

Stage 1: source

_Im_WebSession(starttime=ago(lookback), url_has_any=discord, eventresult='Success')

Stage 2: where

| where isnotempty(Url)

Stage 3: project

| project Url, SrcUsername, SrcIpAddr, TimeGenerated, SrcHostname

Stage 4: where

| where Url has "attachments"

Stage 5: extend

| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url)

Stage 6: summarize

WebData
| summarize
    dcount(Url),
    min(TimeGenerated),
    max(TimeGenerated)
    by DiscordServerId

Stage 7: join

| join kind=inner (WebData) on DiscordServerId

Stage 8: where

where Url has "knownRiskyFileExtensions" and dcount_Url <= 1

Stage 9: summarize

summarize EventCount, EventEndTime, EventStartTime by EventStartTime, EventEndTime, SrcUsername, SrcHostname, SrcIpAddr, Url

Stage 10: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null
Urlmatch
  • attachments transforms: term
  • knownRiskyFileExtensions transforms: term
field:"Url" kind:match
dcount_Urlle
  • 1 transforms: number
field:"dcount_Url" kind:le value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventCountsummarize
EventEndTimesummarize
EventStartTimesummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
Urlsummarize
Nameextend
UPNSuffixextend

TI map Domain entity to Dns Events (ASIM DNS Schema)

#
Severity
medium
Time window
14d
Group by
$left.DomainName, $right.DnsQuery, IndicatorId
Source
github.com/Azure/Azure-Sentinel

Identifies a match in DNS events from any Domain IOC from TI This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM DNS schema'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 999e9f5d-db4a-4b07-a206-29c4e667b7e8
name: TI map Domain entity to Dns Events (ASIM DNS Schema)
description: |
  Identifies a match in DNS events from any Domain IOC from TI
  This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM DNS schema'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: DNS
    dataTypes:
      - DnsEvents
  - connectorId: AzureFirewall
    dataTypes: 
      - AzureDiagnostics
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: InfobloxNIOS
    dataTypes: 
      - Syslog
  - connectorId: GCPDNSDataConnector
    dataTypes: 
      - GCP_DNS_CL
  - connectorId: NXLogDnsLogs
    dataTypes: 
      - NXLog_DNS_Server_CL
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes: 
      - Cisco_Umbrella_dns_CL
  - connectorId: Corelight
    dataTypes: 
      - Corelight_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
tags:
  - Id: 85aca4d1-5d15-4001-abd9-acb86ca1786a
    version: 1.0.0
  - Schema: ASIMDns
    SchemaVersion: 0.1.1
query: |
  let HAS_ANY_MAX = 10000;
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let DomainTIs= ThreatIntelligenceIndicator
    // Picking up only IOC's that contain the entities we want
    | where isnotempty(DomainName)
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  let Domains = DomainTIs | where isnotempty(DomainName) |summarize NDomains=dcount(DomainName), DomainsList=make_set(DomainName) 
    | project DomainList = iff(NDomains > HAS_ANY_MAX, dynamic([]), DomainsList) ;
  DomainTIs
    | join (
        _Im_Dns(starttime=ago(dt_lookBack), domain_has_any=toscalar(Domains))
        | extend DNS_TimeGenerated = TimeGenerated
  ) on $left.DomainName==$right.DnsQuery
  | where DNS_TimeGenerated < ExpirationDateTime
  | project LatestIndicatorTime, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, DNS_TimeGenerated, Dvc, SrcIpAddr, Domain, DnsQuery, DnsQueryType
  | extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Dvc
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain        
customDetails:
  LatestIndicatorTime: LatestIndicatorTime
  Description: Description
  ActivityGroupNames: ActivityGroupNames
  IndicatorId: IndicatorId
  ThreatType: ThreatType
  ExpirationDateTime: ExpirationDateTime
  ConfidenceScore: ConfidenceScore
  DNSRequestTime: DNS_TimeGenerated
  SourceIPAddress: SrcIpAddr
  DnsQuery: DnsQuery
  QueryType: DnsQueryType
version: 1.1.8
kind: Scheduled

Stages and Predicates

Parameters

let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let DomainTIs is inlined into the numbered stages below.

Let Binding: Domains used in Stage 6

let Domains = DomainTIs | where isnotempty(DomainName) |summarize NDomains=dcount(DomainName), DomainsList=make_set(DomainName) 
  | project DomainList = iff(NDomains > HAS_ANY_MAX, dynamic([]), DomainsList);

Stages 1 to 5 define let DomainTIs as the rule's main pipeline source. Stages 6 to 10 use it.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(DomainName)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 5: where

| where Active == true and ExpirationDateTime > now()

Stage 6: join

DomainTIs
| join (
      _Im_Dns(starttime=ago(dt_lookBack), domain_has_any=toscalar(Domains))
      | extend DNS_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.DnsQuery

Stage 7: where

where DNS_TimeGenerated < ExpirationDateTime

Stage 8: project

project ActivityGroupNames, ConfidenceScore, DNS_TimeGenerated, Description, DnsQuery, DnsQueryType, Domain, Dvc, ExpirationDateTime, IndicatorId, LatestIndicatorTime, SrcIpAddr, ThreatType, Url

Stage 9: extend

extend DomainIndex, HostName

Stage 10: extend

extend HostNameDomain
HostNameDomain =
ifDomainIndex != -1substring(Dvc, (DomainIndex + 1))
elseDvc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true corpus 60 (kusto 60)
field:"Active" kind:eq value:"true"
DomainNameis_not_null
  • (no value, null check)
field:"DomainName" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ConfidenceScoreproject
DNS_TimeGeneratedproject
Descriptionproject
DnsQueryproject
DnsQueryTypeproject
Domainproject
Dvcproject
ExpirationDateTimeproject
IndicatorIdproject
LatestIndicatorTimeproject
SrcIpAddrproject
ThreatTypeproject
Urlproject
DomainIndexextend
HostNameextend
HostNameDomainextend

TIE Active Directory attacks pathways

#
Severity
low
Time window
2h
Group by
Severity
Source
github.com/Azure/Azure-Sentinel

'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: de549a62-f595-4810-88bd-621338186588
name: TIE Active Directory attacks pathways
description: |
  'Searches for triggered Indicators of Exposures related to Active Directory attacks pathways.'
severity: Low
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-SDPROP-CONSISTENCY", "C-DANG-PRIMGROUPID", "C-GPO-HARDENING", "C-DC-ACCESS-CONSISTENCY", "C-DANGEROUS-TRUST-RELATIONSHIP", "C-UNCONST-DELEG", "C-ABNORMAL-ENTRIES-IN-SCHEMA"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

XbowCriticalHighFindings

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
high
Time window
1h
Group by
AssetId, FindingId
Source
github.com/Azure/Azure-Sentinel

Creates an incident for each Critical or High severity finding reported by XBOW that is currently in an open state. These findings represent the most severe security issues and require immediate attention. Each alert is deduplicated per finding so re-ingestion of the same finding does not produce duplicate incidents.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: f8e7d6c5-4b3a-4912-8f0e-2d1c3b4a5678
name: XbowCriticalHighFindings
description: |
  Creates an incident for each Critical or High severity finding reported by XBOW that
  is currently in an open state. These findings represent the most severe security issues
  and require immediate attention. Each alert is deduplicated per finding so re-ingestion
  of the same finding does not produce duplicate incidents.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: XbowSecurityConnector
    dataTypes:
      - XbowFindings_CL
      - XbowAssets_CL
queryFrequency: 30m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - PrivilegeEscalation
  - DefenseEvasion
  - Impact
relevantTechniques:
  - T1190
query: |
  XbowFindings_CL
  | where TimeGenerated > ago(1h)
  | where tolower(Severity) in ('critical', 'high')
  | where isempty(State) or tolower(State) == 'open'
  | summarize arg_max(TimeGenerated, *) by FindingId
  | join kind=leftouter (
      XbowAssets_CL
      | summarize arg_max(TimeGenerated, *) by AssetId
      | project AssetId, StartUrl
  ) on AssetId
  | project
      TimeGenerated,
      FindingId,
      FindingName,
      Severity,
      State,
      Summary,
      Impact,
      Mitigations,
      Recipe,
      AssetId,
      AssetName,
      OrganizationId,
      CreatedAt,
      StartUrl
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'XBOW {{Severity}}: {{FindingName}}'
  alertDescriptionFormat: '{{Severity}} severity finding on {{AssetName}}. {{Summary}}'
customDetails:
  FindingID: FindingId
  FindingName: FindingName
  Severity: Severity
  State: State
  AssetID: AssetId
  AssetName: AssetName
  OrganizationID: OrganizationId
  CreatedAt: CreatedAt
  Mitigations: Mitigations
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 24h
    matchingMethod: Selected
    groupByCustomDetails:
      - FindingID
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: StartUrl
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

XbowFindings_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where tolower(Severity) in ('critical', 'high')

Stage 4: where

| where isempty(State) or tolower(State) == 'open'

Stage 5: summarize

| summarize arg_max(TimeGenerated, *) by FindingId

Stage 6: join

| join kind=leftouter (
    XbowAssets_CL
    | summarize arg_max(TimeGenerated, *) by AssetId
    | project AssetId, StartUrl
) on AssetId

Stage 7: project

project AssetId, AssetName, CreatedAt, FindingId, FindingName, Impact, Mitigations, OrganizationId, Recipe, Severity, StartUrl, State, Summary, TimeGenerated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Severityin
  • critical transforms: tolower
  • high transforms: tolower
field:"Severity" kind:in
Stateeq
  • open transforms: tolower
field:"State" kind:eq value:"open"
Stateis_null
  • (no value, null check)
field:"State" kind:is_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AssetIdproject
AssetNameproject
CreatedAtproject
FindingIdproject
FindingNameproject
Impactproject
Mitigationsproject
OrganizationIdproject
Recipeproject
Severityproject
StartUrlproject
Stateproject
Summaryproject
TimeGeneratedproject

Back to all Kusto rules