Detection rules › Kusto

Kusto rules: 34

AWSCloudTrail - Privilege escalation via CRUD Lambda policy

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

Detects inline IAM policy updates that grant broad AWS Lambda lifecycle permissions and related destructive capabilities. This behavior can establish execution control paths and may indicate privilege escalation through unauthorized policy expansion.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: d0953d50-3dc1-4fa3-80fa-4d3e973a0959
name: AWSCloudTrail - Privilege escalation via CRUD Lambda policy
description: |
  Detects inline IAM policy updates that grant broad AWS Lambda lifecycle permissions and related destructive
  capabilities. This behavior can establish execution control paths and may indicate privilege escalation through
  unauthorized policy expansion.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1098.003
query: |
  AWSCloudTrail
  | where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and (Action contains "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "kms:Delete") and Resource == "*" and Condition == ""
  | 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]), "")
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Lambda privilege escalation policy update by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline Lambda escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 4: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 5: mv-expand

| mvexpand Statement

Stage 6: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)

Stage 7: extend

| extend Action = tostring(Action)

Stage 8: where

| where Effect =~ "Allow" and (Action contains "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "kms:Delete") and Resource == "*" and Condition == ""

Stage 9: 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 10: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • kms:Delete
  • lambda:Create
  • lambda:Get
  • lambda:Update
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • PutGroupPolicy corpus 12 (kusto 11, panther 1)
  • PutRolePolicy corpus 12 (kusto 11, panther 1)
  • PutUserPolicy corpus 12 (kusto 11, panther 1)
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend
Statementextend
Actionextend
Conditionextend
Effectextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

Cloudflare - Multiple error requests from single source

#
Status
available
Severity
low
Time window
5m
Group by
SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

'Detects multiple failure requests from single source in short timeframe.'

MITRE ATT&CK Coverage

Rule Body

id: ef877d68-755f-4cf1-ac1d-f336e395667c
name: Cloudflare - Multiple error requests from single source
description: |
  'Detects multiple failure requests from single source in short timeframe.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 100;
  Cloudflare
  | where HttpRequestMethod =~ 'GET'
  | summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)
  | where err_cnt > threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

Cloudflare

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: summarize

| summarize err_cnt = count() by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 4: where

| where err_cnt > threshold

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
err_cntsummarize

Conditional Access - A Conditional Access user/group/role exclusion has changed

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

A Conditional Access user/group/role exclusion has changed in Azure AD.

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
AzureUpdate conditional access policy

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2ce7f00d-3b3c-41b9-ae9a-b79c19d2394e
name: Conditional Access - A Conditional Access user/group/role exclusion has changed
version: 1.0.1
kind: Scheduled
description: A Conditional Access user/group/role exclusion has changed in Azure AD.
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- DefenseEvasion
- CredentialAccess
relevantTechniques:
- T1098
- T1078
query: |+
  // A Conditional Access user/group/role exclusion has changed.
  AuditLogs
  | where OperationName in ("Update conditional access policy")
  | extend excludeUsersOld = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].oldValue))
  | extend excludeGroupsOld = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].oldValue))
  | extend excludeRolesOld = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].oldValue))
  | extend excludeUsersNew = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].newValue))
  | extend excludeGroupsNew = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].newValue))
  | extend excludeRolesNew = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].newValue))
  | where excludeUsersOld != excludeUsersNew or excludeGroupsOld != excludeGroupsNew or excludeRolesOld != excludeRolesNew
  | extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
  | extend accountName = tostring(split(modifiedBy, "@")[0])
  | extend upnSuffix = tostring(split(modifiedBy, "@")[1])
  | project TimeGenerated, OperationName, policy = TargetResources[0].displayName, modifiedBy, accountName, upnSuffix, result = Result,
            excludeUsersOld, excludeUsersNew, excludeGroupsOld, excludeGroupsNew, excludeRolesOld, excludeRolesNew
  | order by TimeGenerated desc
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: accountName
  - identifier: UPNSuffix
    columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName in ("Update conditional access policy")

Stage 3: extend (6 consecutive steps)

| extend excludeUsersOld = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeGroupsOld = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeRolesOld = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].oldValue))
| extend excludeUsersNew = extractjson("$.conditions.users.excludeUsers", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeGroupsNew = extractjson("$.conditions.users.excludeGroups", tostring(TargetResources[0].modifiedProperties[0].newValue))
| extend excludeRolesNew = extractjson("$.conditions.users.excludeRoles", tostring(TargetResources[0].modifiedProperties[0].newValue))

Stage 4: where

| where excludeUsersOld != excludeUsersNew or excludeGroupsOld != excludeGroupsNew or excludeRolesOld != excludeRolesNew

Stage 5: extend (3 consecutive steps)

| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])

Stage 6: project

| project TimeGenerated, OperationName, policy = TargetResources[0].displayName, modifiedBy, accountName, upnSuffix, result = Result,
          excludeUsersOld, excludeUsersNew, excludeGroupsOld, excludeGroupsNew, excludeRolesOld, excludeRolesNew

Stage 7: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNamein
  • Update conditional access policy corpus 6 (kusto 6)
field:"OperationName" kind:in value:"Update conditional access policy"

Output Fields

These fields are emitted when the rule matches.

FieldSource
OperationNameproject
TimeGeneratedproject
accountNameproject
excludeGroupsNewproject
excludeGroupsOldproject
excludeRolesNewproject
excludeRolesOldproject
excludeUsersNewproject
excludeUsersOldproject
modifiedByproject
policyproject
resultproject
upnSuffixproject

CTE - Device License Capacity High (95%)

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

Device license capacity has reached 95%. Emitted once when usage newly crosses this threshold by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); resets and can re-fire if usage later drops back below 95% and crosses again.

Rule Body

id: 5e644219-6067-4849-85df-7458aa575aea
name: CTE - Device License Capacity High (95%)
description: |
  Device license capacity has reached 95%. Emitted once when usage newly crosses this threshold by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js); resets and can re-fire if usage later drops back below 95% and crosses again.
severity: Medium
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 24h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  []
relevantTechniques: []
query: |
  CybereinforceCTE_CL
  | where EventType == "license.capacity_high"
  | extend d = parse_json(Details)
  | extend AppName = "Cybereinforce", ActiveDevices = toint(d.activeDevices), LicensedDevices = toint(d.licensedDevices), UsedPct = todouble(d.usedPct)
  | project TimeGenerated, TenantId, AppName, ActiveDevices, LicensedDevices, UsedPct, Severity, Category, Source
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AppName
      - identifier: InstanceName
        columnName: TenantId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "license.capacity_high"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend AppName = "Cybereinforce", ActiveDevices = toint(d.activeDevices), LicensedDevices = toint(d.licensedDevices), UsedPct = todouble(d.usedPct)

Stage 5: project

| project TimeGenerated, TenantId, AppName, ActiveDevices, LicensedDevices, UsedPct, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • license.capacity_high
field:"EventType" kind:eq value:"license.capacity_high"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActiveDevicesproject
AppNameproject
Categoryproject
LicensedDevicesproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject
UsedPctproject

CTERA Mass Deletions Detection Analytic

#
Status
available
Severity
high
Time window
5m
Group by
UserName
Source
github.com/Azure/Azure-Sentinel

This analytic rule detects and alerts when large amount of deletion operations generated by the CTERA Edge Filer

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 5365f294-0c67-432a-bacf-b1282a3b6c46
name: CTERA Mass Deletions Detection Analytic
description: 'This analytic rule detects and alerts when large amount of deletion operations generated by the CTERA Edge Filer'
kind: Scheduled
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CTERA
    dataTypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 5000
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
    Syslog
    | where ProcessName == 'gw-audit'
    | extend
        TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
        UserName = extract("user=([^|]*)", 1, SyslogMessage),
        Permission = extract("op=([^|]*)", 1, SyslogMessage),
        EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
        RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
        Share = extract("share=([^|]*)", 1, SyslogMessage),
        LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
        Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))
    | where Permission == 'delete'
    | summarize Count = count() by UserName, bin(Timestamp, 5m)
    | where Count > 5000
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: false
        reopenClosedIncident: false
        lookbackDuration: PT5H
        matchingMethod: AllEntities
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride:
    alertnameFormat: 'CTERA Batch Access Denied Detection'
    alertDescriptionFormat: >
      Detected {{Count}} denied access attempts by user {{UserName}} on Edge Filer 
      {{EdgeFiler}} within 5 minutes. Please investigate unauthorized access attempts or misconfigurations.
customDetails:
    TenantName: TenantName
    UserName: UserName
    EdgeFiler: EdgeFiler
    RootPath: RootPath
    Share: Share
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserName
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: EdgeFiler
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where ProcessName == 'gw-audit'

Stage 3: extend

| extend
    TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
    UserName = extract("user=([^|]*)", 1, SyslogMessage),
    Permission = extract("op=([^|]*)", 1, SyslogMessage),
    EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
    RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
    Share = extract("share=([^|]*)", 1, SyslogMessage),
    LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
    Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))

Stage 4: where

| where Permission == 'delete'

Stage 5: summarize

| summarize Count = count() by UserName, bin(Timestamp, 5m)

Stage 6: where

| where Count > 5000

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countsummarize
UserNamesummarize

Detaching Backups Started

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

Detects when a backup file is detached from a backup job.

Rule Body

id: e0d937ca-4582-4c3b-b95b-82fff33ed610
name: Detaching Backups Started
description: Detects when a backup file is detached from a backup job.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 41200\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 41200

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
  MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

Detect PIM Alert Disabling activity

#
Severity
medium
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Privileged Identity Management (PIM) generates alerts when there is suspicious or unsafe activity in Microsoft Entra ID (Azure AD) organization. This query will help detect attackers attempts to disable in product PIM alerts which are associated with Azure MFA requirements and could indicate activation of privileged access

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 1f3b4dfd-21ff-4ed3-8e27-afc219e05c50
name: Detect PIM Alert Disabling activity
description: |
  'Privileged Identity Management (PIM) generates alerts when there is suspicious or unsafe activity in Microsoft Entra ID (Azure AD) organization. 
  This query will help detect attackers attempts to disable in product PIM alerts which are associated with Azure MFA requirements and could indicate activation of privileged access'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T1078
query: |
  AuditLogs
  | where LoggedByService =~ "PIM"
  | where Category =~ "RoleManagement"
  | where ActivityDisplayName has "Disable PIM Alert"
  | extend IpAddress = case(
    isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress), 
    isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
    'Not Available')
  | extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)), 
    tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
  | project InitiatedBy, ActivityDateTime, ActivityDisplayName, IpAddress, AADOperationType, AADTenantId, ResourceId, CorrelationId, Identity
  | extend AccountName = tostring(split(InitiatedBy, "@")[0]), AccountUPNSuffix = tostring(split(InitiatedBy, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatedBy
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where LoggedByService =~ "PIM"

Stage 3: where

| where Category =~ "RoleManagement"

Stage 4: where

| where ActivityDisplayName has "Disable PIM Alert"

Stage 5: extend

| extend IpAddress = case(
  isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.user)).ipAddress), 
  isnotempty(tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)) and tostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != 'null', tostring(parse_json(tostring(InitiatedBy.app)).ipAddress),
  'Not Available')
IpAddress =
iftostring(parse_json(tostring(InitiatedBy.user)).ipAddress) != "null"tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
eliftostring(parse_json(tostring(InitiatedBy.app)).ipAddress) != "null"tostring(parse_json(tostring(InitiatedBy.app)).ipAddress)
else'Not Available'

Stage 6: extend

| extend InitiatedBy = iff(isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)), 
  tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName), tostring(parse_json(tostring(InitiatedBy.app)).displayName)), UserRoles = tostring(parse_json(tostring(InitiatedBy.user)).ipAddress)
InitiatedBy =
if/* macro: isnotempty(tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)) */tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
elsetostring(parse_json(tostring(InitiatedBy.app)).displayName)

Stage 7: project

| project InitiatedBy, ActivityDateTime, ActivityDisplayName, IpAddress, AADOperationType, AADTenantId, ResourceId, CorrelationId, Identity

Stage 8: extend

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADOperationTypeproject
AADTenantIdproject
ActivityDateTimeproject
ActivityDisplayNameproject
CorrelationIdproject
Identityproject
InitiatedByproject
IpAddressproject
ResourceIdproject
AccountNameextend
AccountUPNSuffixextend

GSA - Detect Connections Outside Operational Hours

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

This query identifies connections that occur outside of the defined operational hours. It helps in monitoring and flagging any unusual activity that may occur during non-business hours, indicating potential security concerns or policy violations.

MITRE ATT&CK Coverage

Rule Body

id: 4c9f0a9e-44d7-4c9b-b7f0-f6a6e0d8f8fa
name: GSA - Detect Connections Outside Operational Hours
description: |
  This query identifies connections that occur outside of the defined operational hours. It helps in monitoring and flagging any unusual activity that may occur during non-business hours, indicating potential security concerns or policy violations.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - NetworkAccessTrafficLogs
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
  - T1133
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let operational_start_hour = 8; // Start of operational hours (8 AM)
  let operational_end_hour = 18; // End of operational hours (6 PM)
  NetworkAccessTraffic
  | where TimeGenerated between (starttime .. endtime)
  | extend HourOfDay = datetime_part('hour', TimeGenerated)
  | where HourOfDay < operational_start_hour or HourOfDay >= operational_end_hour
  | project TimeGenerated, UserPrincipalName, SourceIp, DestinationIp, DestinationPort, Action, DeviceId, DeviceOperatingSystem, ConnectionId
  | extend IPCustomEntity = SourceIp, AccountCustomEntity = UserPrincipalName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let operational_start_hour = 8;
let operational_end_hour = 18;

Stage 1: source

NetworkAccessTraffic

Stage 2: where

| where TimeGenerated between (starttime .. endtime)

Stage 3: extend

| extend HourOfDay = datetime_part('hour', TimeGenerated)

Stage 4: where

| where HourOfDay < operational_start_hour or HourOfDay >= operational_end_hour

Stage 5: project

| project TimeGenerated, UserPrincipalName, SourceIp, DestinationIp, DestinationPort, Action, DeviceId, DeviceOperatingSystem, ConnectionId

Stage 6: extend

| extend IPCustomEntity = SourceIp, AccountCustomEntity = UserPrincipalName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ConnectionIdproject
DestinationIpproject
DestinationPortproject
DeviceIdproject
DeviceOperatingSystemproject
SourceIpproject
TimeGeneratedproject
UserPrincipalNameproject
AccountCustomEntityextend
IPCustomEntityextend

Imperva - Malicious Client

#
Status
available
Severity
high
Time window
10m
Source
github.com/Azure/Azure-Sentinel

'Detects connections from known malicious clients.'

MITRE ATT&CK Coverage

Rule Body

id: 2ff35ed4-b26a-4cad-93a6-f67adb00e919
name: Imperva - Malicious Client
description: |
  'Detects connections from known malicious clients.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: ImpervaWAFCloudAPI
    dataTypes:
      - ImpervaWAFCloud
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  ImpervaWAFCloud
  | where ClientApp in~ ('VulnerabilityScanner', 'DDoSBot', 'ClickBot','CommentSpamBot','HackingTool', 'SpamBot', 'Worm')
  | where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'
  | extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = QueryString
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

ImpervaWAFCloud

Stage 2: where

| where ClientApp in~ ('VulnerabilityScanner', 'DDoSBot', 'ClickBot','CommentSpamBot','HackingTool', 'SpamBot', 'Worm')

Stage 3: where

| where DvcAction !startswith 'REQ_BLOCKED' or DvcAction !startswith 'REQ_BAD_'

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = QueryString

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientAppin
  • ClickBot
  • CommentSpamBot
  • DDoSBot
  • HackingTool
  • SpamBot
  • VulnerabilityScanner
  • Worm
field:"ClientApp" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend
UrlCustomEntityextend

Jira - Permission scheme updated

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

'Detects when permission scheme was updated.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 72592618-fa57-45e1-9f01-ca8706a5e3f5
name: Jira - Permission scheme updated
description: |
  'Detects when permission scheme was updated.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: JiraAuditAPI
    dataTypes:
      - JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1531
query: |
  JiraAudit
  | where EventMessage =~ 'Permission scheme updated'
  | project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, ChangedValues
  | extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

JiraAudit

Stage 2: where

| where EventMessage =~ 'Permission scheme updated'

Stage 3: project

| project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, ChangedValues

Stage 4: extend

| extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ChangedValuesproject
EventCreationTimeproject
ObjectItemNameproject
SrcIpAddrproject
UserNameproject
AccountCustomEntityextend
IPCustomEntityextend

Potential DGA detected (ASIM DNS Schema)

#
Severity
medium
Time window
10d
Group by
SrcIpAddr, dcount_sld
Author
Yaron
Source
github.com/Azure/Azure-Sentinel

Identifies clients with a high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). Alert is generated when a new IP address is seen (based on not being seen associated with NXDomain records in prior 10-day baseline period). 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: 983a6922-894d-413c-9f04-d7add0ecc307
name: Potential DGA detected (ASIM DNS Schema)
description: |
  'Identifies clients with a high NXDomain count which could be indicative of a DGA (cycling through possible C2 domains where most C2s are not live). Alert is generated when a new IP address is seen (based on not being seen associated with 
  NXDomain records in prior 10-day baseline period).
  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: 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: CiscoUmbrellaDataConnector
    dataTypes: 
      - Cisco_Umbrella_dns_CL
  - connectorId: Corelight
    dataTypes: 
      - Corelight_CL

queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1008
tags:
  - ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/DnsEvents/DNS_HighNXDomainCount_detection.yaml
    version: 1.0.0
  - Schema: ASIMDns
    SchemaVersion: 0.1.1
query: |
  let referencestarttime = 10d;
  let referenceendtime = 1d;
  let threshold = 100;
  let nxDomainDnsEvents = (stime:datetime, etime:datetime) 
    {_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)
    | where DnsQueryTypeName in ("A", "AAAA")
    | where ipv4_is_match("127.0.0.1", SrcIpAddr) == False
    | where DnsQuery !contains "/" and  DnsQuery contains "."};
  nxDomainDnsEvents (stime=ago(referenceendtime) ,etime=now())
    | extend sld = tostring(split(DnsQuery, ".")[-2])
    | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(sld) by SrcIpAddr
    | where dcount_sld > threshold
    // Filter out previously seen IPs
    | join kind=leftanti (nxDomainDnsEvents (stime=ago(referencestarttime), etime=ago(referenceendtime))
      | extend sld = tostring(split(DnsQuery, ".")[-2])
      | summarize dcount(sld) by SrcIpAddr
      | where dcount_sld > threshold ) on SrcIpAddr
  // Pull out sample NXDomain responses for those remaining potentially infected IPs
  | join kind = inner (nxDomainDnsEvents (stime=ago(referencestarttime), etime=now()) | summarize by DnsQuery, SrcIpAddr) on SrcIpAddr
  | summarize StartTime = min(StartTime), EndTime = max(EndTime), sampleNXDomainList=make_list(DnsQuery, 100)  by SrcIpAddr, dcount_sld
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.3.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Yaron
    support:
        tier: Community
    categories:
        domains: [ "Security - Network" ]

Stages and Predicates

Parameters

let referencestarttime = 10d;
let referenceendtime = 1d;
let threshold = 100;

let nxDomainDnsEvents is inlined into the numbered stages below.

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

Stage 1: source

_Im_Dns(responsecodename='NXDOMAIN', starttime=stime, endtime=etime)

Stage 2: where

| where DnsQueryTypeName in ("A", "AAAA")

Stage 3: where

| where ipv4_is_match("127.0.0.1", SrcIpAddr) == False

Stage 4: where

| where DnsQuery !contains "/" and  DnsQuery contains "."

Stage 5: extend

nxDomainDnsEvents
| extend sld = tostring(split(DnsQuery, ".")[-2])

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(sld) by SrcIpAddr

Stage 7: where

| where dcount_sld > threshold

Stage 8: join (negated)

join kind=leftanti (nxDomainDnsEvents) on SrcIpAddr

Stage 9: join

| join kind = inner (nxDomainDnsEvents (stime=ago(referencestarttime), etime=now()) | summarize by DnsQuery, SrcIpAddr) on SrcIpAddr

Stage 10: summarize

summarize EndTime, StartTime, sampleNXDomainList by SrcIpAddr, dcount_sld

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DnsQuerycontains
  • . corpus 6 (elastic 3, kusto 2, splunk 1)
field:"QueryName" kind:contains value:"."
DnsQueryTypeNamein
  • A corpus 2 (kusto 2)
  • AAAA corpus 2 (kusto 2)
field:"DnsQueryTypeName" kind:in
dcount_sldgt
  • 100 transforms: number
field:"dcount_sld" kind:gt value:"100"

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
SrcIpAddrsummarize
StartTimesummarize
dcount_sldsummarize
sampleNXDomainListsummarize

SAP BTP - Failed access attempts across multiple BAS subaccounts

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

Identifies failed Business Application Studio access attempts over a predefined number of subaccounts.

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance
Discovery

Rule Body

id: 74b243a6-3046-48aa-8b03-e43b3c529cc1
kind: Scheduled
name: SAP BTP - Failed access attempts across multiple BAS subaccounts
description: Identifies failed Business Application Studio access attempts over a
  predefined number of subaccounts.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
  - Discovery
relevantTechniques:
  - T1595
  - T1526
query: |
  let subaccount_detection_threshold = 3;
  SAPBTPAuditLog_CL
  | where Category == "audit.security-events" and Message has "Unauthorized access attempt"
  | summarize Start=min(UpdatedOn), End=max(UpdatedOn), Tenants = make_set(Tenant, 100) by UserName
  | where array_length(Tenants) > subaccount_detection_threshold
  | project Start, End, UserName, Tenants, CloudApp = "SAP BTP"
  | extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
alertDetailsOverride:
  alertDisplayNameFormat: SAP BTP - Unauthorized access attempt to multiple tenants
  alertDescriptionFormat: '{{UserName}} attempted, and failed, to log into multiple
    Business Application Studio dev spaces. Tenants accessed: {{Tenants}}'
version: 3.0.6

Stages and Predicates

Parameters

let subaccount_detection_threshold = 3;

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: where

| where Category == "audit.security-events" and Message has "Unauthorized access attempt"

Stage 3: summarize

| summarize Start=min(UpdatedOn), End=max(UpdatedOn), Tenants = make_set(Tenant, 100) by UserName

Stage 4: where

| where array_length(Tenants) > subaccount_detection_threshold

Stage 5: project

| project Start, End, UserName, Tenants, CloudApp = "SAP BTP"

Stage 6: extend

| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • audit.security-events
field:"Category" kind:eq value:"audit.security-events"
Messagematch
  • Unauthorized access attempt transforms: term
field:"Message" kind:match value:"Unauthorized access attempt"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CloudAppproject
Endproject
Startproject
Tenantsproject
UserNameproject
AccountNameextend
UPNSuffixextend

SAP ETD - Execution of Sensitive Function Module

#

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
medium
Time window
1h
Source
github.com/Azure/Azure-Sentinel

Identifies execution of a sensitive ABAP Function Module using the watchlists provided by the Microsoft Sentinel Solution for SAP Source Action: Execute a sensitive function module directly using SE37. Data Sources: SAP Enterprise Thread Detection Solution - Alerts

MITRE ATT&CK Coverage

TacticTechniques
DiscoveryNo specific technique

Rule Body

id: c6111e06-11e2-45eb-86ef-28313a06db35
kind: Scheduled
name: SAP ETD - Execution of Sensitive Function Module
description: |
  Identifies execution of a sensitive ABAP Function Module using the watchlists provided by the Microsoft Sentinel Solution for SAP
  
  Source Action: Execute a sensitive function module directly using SE37.
  
  *Data Sources: SAP Enterprise Thread Detection Solution -  Alerts*
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPETDAlerts
    dataTypes:
      - SAPETDAlerts_CL
queryFrequency: 5m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics: 
  - Discovery
relevantTechniques: []
query: |
  let AuditTimeAgo = 60m;
  let minThreshold= 1;
  let minScore= 50;
  let SenseModules = _GetWatchlist('SAP - Sensitive Function Modules')
  | project trimmedFunctionModule = trim(@"\s+", FunctionModule);
  SAPETDAlerts_CL
  | where TimeGenerated > ago(AuditTimeAgo)
  | where Threshold >= minThreshold and Score >= minScore
  | where PatternName == "Critical Function module call in Test framework calls"
  | mv-expand NormalizedTriggeringEvents
  | extend sapOriginalEvent = tostring(NormalizedTriggeringEvents.OriginalEvent)
  | extend Id_ = NormalizedTriggeringEvents.Id
  | extend extracted_user_ip = tostring(NormalizedTriggeringEvents.NetworkIPAddressInitiator)
  | extend extracted_sap_user = tostring(NormalizedTriggeringEvents.UserAccountActing)
  | extend extracted_sid = tostring(NormalizedTriggeringEvents.SystemIdActor)
  | extend extracted_instance_name = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)
  | extend extracted_instance_host = tostring(NormalizedTriggeringEvents.ServiceInstanceName)
  | extend extracted_function_module = trim(@"\s+", tostring(NormalizedTriggeringEvents.ServiceFunctionName))
  | extend extracted_system_role = tostring(NormalizedTriggeringEvents.SystemRoleReporter)
  | where extracted_function_module in (SenseModules)
  | extend AlertName = strcat("SAP ETD - Sensitive Function Module ", extracted_function_module," was executed by user ", extracted_sap_user, 
  " in a ", tolower(extracted_system_role), " system"), Dummy = " "
  | mv-expand Users
  | extend
    UserAccountName = tostring(Users.UserAccountName),
    UserEmail = tostring(Users.EmailAddresses[0])
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserAccountName
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: UserEmail
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: extracted_sid
      - identifier: InstanceName
        columnName: extracted_instance_name
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: extracted_instance_host
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: extracted_user_ip
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP ETD - {{PatternName}} '
  alertDescriptionFormat: |
    {{PatternDescription}}

    Source: SAP ETD
customDetails:
  SAP_User: extracted_sap_user
  SAP_UserEmail: UserEmail
  ETD_AlertNumber: AlertId
version: 1.0.1

Stages and Predicates

Parameters

let AuditTimeAgo = 60m;
let minThreshold = 1;
let minScore = 50;

Let Binding: SenseModules used in Stage 7

let SenseModules = _GetWatchlist('SAP - Sensitive Function Modules')
| project trimmedFunctionModule = trim(@"\s+", FunctionModule);

Stage 1: source

SAPETDAlerts_CL

Stage 2: where

| where TimeGenerated > ago(AuditTimeAgo)

Stage 3: where

| where Threshold >= minThreshold and Score >= minScore

Stage 4: where

| where PatternName == "Critical Function module call in Test framework calls"

Stage 5: mv-expand

| mv-expand NormalizedTriggeringEvents

Stage 6: extend (9 consecutive steps)

| extend sapOriginalEvent = tostring(NormalizedTriggeringEvents.OriginalEvent)
| extend Id_ = NormalizedTriggeringEvents.Id
| extend extracted_user_ip = tostring(NormalizedTriggeringEvents.NetworkIPAddressInitiator)
| extend extracted_sap_user = tostring(NormalizedTriggeringEvents.UserAccountActing)
| extend extracted_sid = tostring(NormalizedTriggeringEvents.SystemIdActor)
| extend extracted_instance_name = tostring(NormalizedTriggeringEvents.NetworkHostnameActor)
| extend extracted_instance_host = tostring(NormalizedTriggeringEvents.ServiceInstanceName)
| extend extracted_function_module = trim(@"\s+", tostring(NormalizedTriggeringEvents.ServiceFunctionName))
| extend extracted_system_role = tostring(NormalizedTriggeringEvents.SystemRoleReporter)

Stage 7: where

| where extracted_function_module in (SenseModules)

Stage 8: extend

| extend AlertName = strcat("SAP ETD - Sensitive Function Module ", extracted_function_module," was executed by user ", extracted_sap_user, 
" in a ", tolower(extracted_system_role), " system"), Dummy = " "

Stage 9: mv-expand

| mv-expand Users

Stage 10: extend

| extend
  UserAccountName = tostring(Users.UserAccountName),
  UserEmail = tostring(Users.EmailAddresses[0])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
sapOriginalEventextend
Id_extend
extracted_user_ipextend
extracted_sap_userextend
extracted_sidextend
extracted_instance_nameextend
extracted_instance_hostextend
extracted_function_moduleextend
extracted_system_roleextend
AlertNameextend
Dummyextend
UserAccountNameextend
UserEmailextend

Snowflake - Possible data destraction

#
Status
available
Severity
medium
Time window
5m
Group by
TargetUsername
Source
github.com/Azure/Azure-Sentinel

'Detects possible data destruction.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: c2f93727-e4b0-4cb9-8f80-f52ebbd96ece
name: Snowflake - Possible data destraction
description: |
  'Detects possible data destruction.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Snowflake
    dataTypes:
      - Snowflake
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
  let threshold = 10;
  Snowflake
  | where QueryType =~ 'DROP'
  | where QueryExecutionStatus =~ 'SUCCESS'
  | summarize count() by TargetUsername, bin(TimeGenerated, 5m)
  | where count_ > threshold
  | extend AccountCustomEntity = TargetUsername
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

Snowflake

Stage 2: where

| where QueryType =~ 'DROP'

Stage 3: where

| where QueryExecutionStatus =~ 'SUCCESS'

Stage 4: summarize

| summarize count() by TargetUsername, bin(TimeGenerated, 5m)

Stage 5: where

| where count_ > threshold

Stage 6: extend

| extend AccountCustomEntity = TargetUsername

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
TargetUsernamesummarize
AccountCustomEntityextend

T1558.003 - Kerberoasting

#
Group by
TargetUserName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Detects kerberoasting by using time-series analysis functions. Highly accurate in big environments. Step by step explanation is in the query to make it easy to understand.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

// create a whitelist that consists of your AD domain names. ex: ("contoso.com","contoso.local","contoso.dmz")
let _whitelist_ServiceName = dynamic (["contoso.com","contoso.local","contoso.dmz"]);
let _fromDate = ago(8d);
let _thruDate = now();
let _min_dcount_threshold = 6;
let _period =2h; // this value should also be used as the rule frequency.
// If an account requested more than <_min_dcount_threshold> service tickets during the last <_period> hour, 
// we need to check if it is suspicious or not by looking at historical data for the same user. 
// STEP 1: Create a list of accounts to be checked based on their requests during the last <_period> hour.
// This approach also increases the rule performance as only the suspicious accounts are checked historically.
let _accounts_to_be_checked =
    SecurityEvent
    | where TimeGenerated > ago(_period)
    | where EventID == 4769
    | parse EventData with * 'Status">' Status "<" *
    | where Status == '0x0'
    | parse EventData with * 'ServiceName">' ServiceName "<" *
    | where ServiceName !contains "$" and ServiceName !contains "krbtgt"
    | where ServiceName !in~ (_whitelist_ServiceName )
    | parse EventData with * 'TargetUserName">' TargetUserName "<" *
    | where TargetUserName !contains ServiceName
    | parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
    | where TicketEncryptionType in~ ('0x17','0x18') //these are the weak encryption types.
    | parse EventData with * 'TicketOptions">' TicketOptions "<" *
    | parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
    | summarize dcount(ServiceName) by TargetUserName
    | where dcount_ServiceName > _min_dcount_threshold
    | summarize make_list(TargetUserName);
// STEP 2: Create a time-series statistics per day for each account in the list, for the last 8 days.
let baseData = materialize(
    SecurityEvent
    | where TimeGenerated > _fromDate
    | where EventID == 4769
    | parse EventData with * 'Status">' Status "<" *
    | where Status == '0x0'
    | parse EventData with * 'ServiceName">' ServiceName "<" *
    | where ServiceName !contains "$" and ServiceName !contains "krbtgt"
    | where ServiceName !in~ (_whitelist_ServiceName )
    | parse EventData with * 'TargetUserName">' TargetUserName "<" *
    | where TargetUserName in~ (_accounts_to_be_checked) // only the accounts in the list will be analyzed.
    | parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
    | where TicketEncryptionType in~ ('0x17','0x18')
    | parse EventData with * 'TicketOptions">' TicketOptions "<" *
    | parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
    //creating the time-series stats
    | make-series dcount(ServiceName) default=0  on TimeGenerated in range(_fromDate, _thruDate, 1d)  by TargetUserName, ClientIPAddress 
    | extend avg = todouble(series_stats_dynamic(dcount_ServiceName).avg )
    );
// STEP 3: Find outliers(anomalies) in the time-series data and make a list of suspicious accounts.
let AnomTargetUserNames = (
    baseData
    | extend outliers = series_outliers(dcount_ServiceName)
    | mvexpand dcount_ServiceName, TimeGenerated, outliers to typeof(double)
    // dcount must be at least greater than the threshold. otherwise small numbers can also be an outlier.
	// ex: 1,1,1,1,5 -> 5 is an outlier but probably not suspicious in big environment
    | where dcount_ServiceName > _min_dcount_threshold
    // time-series function rounds the time information and displays the time info as the startofday like '2020-08-23T00:00:00.0000000Z'. 
	// check if the anomlay happened on the current day.
    | where TimeGenerated >= startofday(now())
    // we are looking for positive outliers, meaning increase in the volume.
    // this threshold can be adjusted based on the number of false positives or false negatives
    | where outliers > 1.5
    // we are looking for big outliers. Top 25 outliers should be more than enough. 
    | top 25 by outliers desc
    | summarize make_list(TargetUserName)
    );
// STEP 4: Anomaly is found but it's not clear when it happened exactly and there is not enough information for incident response.
//         Get last <_period> of data and dcount of service names, set of service names that have been requested for each account in the anomalous account list.
//         Join with the baseData as it has both avg values and other details of the anomaly. 
//         Avg value will also be used for preventing the rule from triggering the same alert again.  
//         We already found the anomalous users but we need to check if the anomaly happened in the last <_period> to prevent duplicate incidents.
SecurityEvent
| where TimeGenerated >= ago(_period)
| where EventID == 4769
| parse EventData with * 'Status">' Status "<" *
| where Status == '0x0'
| parse EventData with * 'ServiceName">' ServiceName "<" *
| where ServiceName !contains "$" and ServiceName !contains "krbtgt"
| where ServiceName !in~ (_whitelist_ServiceName )
| parse EventData with * 'TargetUserName">' TargetUserName "<" *
| where TargetUserName in (AnomTargetUserNames) // checking if the user is in the list of anomolous users
| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
| where TicketEncryptionType in~ ('0x17','0x18')
| parse EventData with * 'TicketOptions">' TicketOptions "<" *
| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
// Calculate the dcount of service tickets, create the set of service tickets requested
| summarize start_Time=min(TimeGenerated), end_Time=max(TimeGenerated), ServiceNameCount_LastPeriod = dcount(ServiceName), ServiceNameSet_LastPeriod = makeset(ServiceName) by TargetUserName
// Now join this with the baseData to display the details of the anomaly.
| join kind=leftouter baseData on TargetUserName
| where ServiceNameCount_LastPeriod > avg + 2 // this is a simple check to see if the anomaly happened in the last period. 
// Display the columns we need
| project start_Time, end_Time, ClientIPAddress, TargetUserName, ServiceNameCount_Avg = avg, ServiceNameCount_LastPeriod, dcount_ServiceName, ServiceNameSet_LastPeriod

Stages and Predicates

Parameters

let _whitelist_ServiceName = dynamic (["contoso.com","contoso.local","contoso.dmz"]);
let _fromDate = ago(8d);
let _thruDate = now();
let _min_dcount_threshold = 6;
let _period = 2h;

Let Binding: _accounts_to_be_checked used in Stage 1

let _accounts_to_be_checked = SecurityEvent
    | where TimeGenerated > ago(_period)
    | where EventID == 4769
    | parse EventData with * 'Status">' Status "<" *
    | where Status == '0x0'
    | parse EventData with * 'ServiceName">' ServiceName "<" *
    | where ServiceName !contains "$" and ServiceName !contains "krbtgt"
    | where ServiceName !in~ (_whitelist_ServiceName )
    | parse EventData with * 'TargetUserName">' TargetUserName "<" *
    | where TargetUserName !contains ServiceName
    | parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
    | where TicketEncryptionType in~ ('0x17','0x18')
    | parse EventData with * 'TicketOptions">' TicketOptions "<" *
    | parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
    | summarize dcount(ServiceName) by TargetUserName
    | where dcount_ServiceName > _min_dcount_threshold
    | summarize make_list(TargetUserName);

Let Binding: baseData used in Stages 2, 19

let baseData = materialize(
    SecurityEvent
    | where TimeGenerated > _fromDate
    | where EventID == 4769
    | parse EventData with * 'Status">' Status "<" *
    | where Status == '0x0'
    | parse EventData with * 'ServiceName">' ServiceName "<" *
    | where ServiceName !contains "$" and ServiceName !contains "krbtgt"
    | where ServiceName !in~ (_whitelist_ServiceName )
    | parse EventData with * 'TargetUserName">' TargetUserName "<" *
    | where TargetUserName in~ (_accounts_to_be_checked)
    | parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *
    | where TicketEncryptionType in~ ('0x17','0x18')
    | parse EventData with * 'TicketOptions">' TicketOptions "<" *
    | parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *
    | make-series dcount(ServiceName) default=0  on TimeGenerated in range(_fromDate, _thruDate, 1d)  by TargetUserName, ClientIPAddress 
    | extend avg = todouble(series_stats_dynamic(dcount_ServiceName).avg )
    );

Let Binding: AnomTargetUserNames used in Stages 3, 13

let AnomTargetUserNames = (
    baseData
    | extend outliers = series_outliers(dcount_ServiceName)
    | mvexpand dcount_ServiceName, TimeGenerated, outliers to typeof(double)
    | where dcount_ServiceName > _min_dcount_threshold
    | where TimeGenerated >= startofday(now())
    | where outliers > 1.5
    | top 25 by outliers desc
    | summarize make_list(TargetUserName)
    );

Stage 1: source

let _accounts_to_be_checked

Stage 2: source

let baseData

Stage 3: source

let AnomTargetUserNames

Stage 4: source

SecurityEvent

Stage 5: where

| where TimeGenerated >= ago(_period)

Stage 6: where

| where EventID == 4769

Stage 7: parse

| parse EventData with * 'Status">' Status "<" *

Stage 8: where

| where Status == '0x0'

Stage 9: parse

| parse EventData with * 'ServiceName">' ServiceName "<" *

Stage 10: where

| where ServiceName !contains "$" and ServiceName !contains "krbtgt"

Stage 11: where

| where ServiceName !in~ (_whitelist_ServiceName )

Stage 12: parse

| parse EventData with * 'TargetUserName">' TargetUserName "<" *

Stage 13: where

| where TargetUserName in (AnomTargetUserNames)

Stage 14: parse

| parse EventData with * 'TicketEncryptionType">' TicketEncryptionType "<" *

Stage 15: where

| where TicketEncryptionType in~ ('0x17','0x18')

Stage 16: parse

| parse EventData with * 'TicketOptions">' TicketOptions "<" *

Stage 17: parse

| parse EventData with * 'IpAddress">::ffff:' ClientIPAddress "<" *

Stage 18: summarize

| summarize start_Time=min(TimeGenerated), end_Time=max(TimeGenerated), ServiceNameCount_LastPeriod = dcount(ServiceName), ServiceNameSet_LastPeriod = makeset(ServiceName) by TargetUserName

Stage 19: join

| join kind=leftouter baseData on TargetUserName

Stage 20: where

where ServiceNameCount_LastPeriod > (avg + 2)

Stage 21: project

project ClientIPAddress, ServiceNameCount_Avg, ServiceNameCount_LastPeriod, ServiceNameSet_LastPeriod, TargetUserName, dcount_ServiceName, end_Time, start_Time

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPAddressproject
ServiceNameCount_Avgproject
ServiceNameCount_LastPeriodproject
ServiceNameSet_LastPeriodproject
TargetUserNameproject
dcount_ServiceNameproject
end_Timeproject
start_Timeproject

TI map File Hash to DeviceFileEvents Event

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

Identifies a match in DeviceFileEvents Event data from any FileHash IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: bc0eca2e-db50-44e6-8fa3-b85f91ff5ee7
name: TI map File Hash to DeviceFileEvents Event
description: |
  'Identifies a match in DeviceFileEvents Event data from any FileHash IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceFileEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let DeviceFileEvents_ = (union
  (DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA1) | extend FileHashValue = SHA1),
  (DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA256) | extend FileHashValue = SHA256));
  let Hashes = DeviceFileEvents_ | distinct FileHashValue;
  ThreatIntelligenceIndicator
  | where isnotempty(FileHashValue)
  | where TimeGenerated > ago(ioc_lookBack)
  | where FileHashValue in (Hashes)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  | join kind=innerunique (DeviceFileEvents_) on $left.FileHashValue == $right.FileHashValue
  | where TimeGenerated < ExpirationDateTime
  | summarize TimeGenerated = arg_max(TimeGenerated, *) by  IndicatorId, DeviceId
  | project TimeGenerated, TrafficLightProtocolLevel, Description, ActivityGroupNames, IndicatorId, ThreatType, FileHashValue, FileHashType, ExpirationDateTime, ConfidenceScore, ActionType, DeviceId, DeviceName, FolderPath, RequestAccountDomain, RequestAccountName, RequestAccountSid, MachineGroup
  | extend timestamp = TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: RequestAccountName
      - identifier: Sid
        columnName: RequestAccountSid
      - identifier: NTDomain
        columnName: RequestAccountDomain
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: FileHashValue
      - identifier: Algorithm
        columnName: FileHashType
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: DeviceFileEvents_ used in Stages 1, 10

let DeviceFileEvents_ = (union
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA1) | extend FileHashValue = SHA1),
(DeviceFileEvents | where TimeGenerated > ago(dt_lookBack) | where isnotempty(SHA256) | extend FileHashValue = SHA256));

Let Binding: Hashes used in Stages 2, 6

let Hashes = DeviceFileEvents_ | distinct FileHashValue;

Stage 1: source

let DeviceFileEvents_

Stage 2: source

let Hashes

Stage 3: source

ThreatIntelligenceIndicator

Stage 4: where

where isnotempty(FileHashValue)

Stage 5: where

where TimeGenerated > ago(1209600s)

Stage 6: where

where FileHashValue == "Hashes"

Stage 7: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 8: where

where Active == true

Stage 9: where

where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")

Stage 10: join

join kind=innerunique (DeviceFileEvents_) on $left.FileHashValue, $right.FileHashValue

Stage 11: where

where TimeGenerated < ExpirationDateTime

Stage 12: summarize

summarize TimeGenerated by IndicatorId, DeviceId

Stage 13: project

project ActionType, ActivityGroupNames, ConfidenceScore, Description, DeviceId, DeviceName, ExpirationDateTime, FileHashType, FileHashValue, FolderPath, IndicatorId, MachineGroup, RequestAccountDomain, RequestAccountName, RequestAccountSid, ThreatType, TimeGenerated, TrafficLightProtocolLevel

Stage 14: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionTypeproject
ActivityGroupNamesproject
ConfidenceScoreproject
Descriptionproject
DeviceIdproject
DeviceNameproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
FolderPathproject
IndicatorIdproject
MachineGroupproject
RequestAccountDomainproject
RequestAccountNameproject
RequestAccountSidproject
ThreatTypeproject
TimeGeneratedproject
TrafficLightProtocolLevelproject
timestampextend

TI Map IP Entity to W3CIISLog

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.cIP, Id, ObservableValue, cIP
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 888c4736-e604-48eb-b2c7-3462356d9510
name: TI Map IP Entity to W3CIISLog
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for W3CIISLog events
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelIndicators
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
       | extend NetworkSourceIP = toupper(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
    | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
  // Perform a join between IP indicators and W3CIISLog events
  IP_Indicators
    | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        W3CIISLog
        | where TimeGenerated >= ago(dt_lookBack)
        | where isnotempty(cIP)
        | where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
        | extend W3CIISLog_TimeGenerated = TimeGenerated
    )
    on $left.TI_ipEntity == $right.cIP
    // Filter out W3CIISLog events that occurred after the expiration of the corresponding indicator
    | where W3CIISLog_TimeGenerated < ValidUntil
    // Group the results by IndicatorId and keep the W3CIISLog event with the latest timestamp
    | summarize W3CIISLog_TimeGenerated = arg_max(W3CIISLog_TimeGenerated, *) by Id, cIP
    // Select the desired output fields
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project timestamp = W3CIISLog_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
      TI_ipEntity, Computer, sSiteName, cIP, sIP, sPort, csMethod, csUserName, scStatus, scSubStatus, scWin32Status,
     NetworkSourceIP, Type, Url
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: csUserName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: cIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.6
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 10 define let IP_Indicators as the rule's main pipeline source. Stages 11 to 17 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")

Stage 4: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
TI_ipEntity =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: where

| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 9: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 10: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 11: project-reorder

IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity

Stage 12: join

| join kind=innerunique (
      W3CIISLog
      | where TimeGenerated >= ago(dt_lookBack)
      | where isnotempty(cIP)
      | where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
      | extend W3CIISLog_TimeGenerated = TimeGenerated
  )
  on $left.TI_ipEntity == $right.cIP

Stage 13: where

where W3CIISLog_TimeGenerated < ValidUntil

Stage 14: summarize

summarize W3CIISLog_TimeGenerated by Id, cIP

Stage 15: extend

extend Description

Stage 16: extend

extend ActivityGroupNames

Stage 17: project

project ActivityGroupNames, Computer, Confidence, Description, Id, NetworkSourceIP, TI_ipEntity, Type, Url, ValidUntil, cIP, csMethod, csUserName, sIP, sPort, sSiteName, scStatus, scSubStatus, scWin32Status, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null
cIPis_not_null
  • (no value, null check)
field:"cIP" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_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:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"
cIPcidr_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:cIP
cIPstarts_with127.excludes:cIP field:"cIP" value:"127."
cIPstarts_with::excludes:cIP field:"cIP" value:"::"
cIPstarts_withfe80excludes:cIP field:"cIP" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Computerproject
Confidenceproject
Descriptionproject
Idproject
NetworkSourceIPproject
TI_ipEntityproject
Typeproject
Urlproject
ValidUntilproject
cIPproject
csMethodproject
csUserNameproject
sIPproject
sPortproject
sSiteNameproject
scStatusproject
scSubStatusproject
scWin32Statusproject
timestampproject

Trend Micro CAS - Unexpected file via mail

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

'Detects when unexpected file received via mail.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 201fd2d1-9131-4b29-bace-ce5d19f3e4ee
name: Trend Micro CAS - Unexpected file via mail
description: |
  'Detects when unexpected file received via mail.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);
  TrendMicroCAS
  | where EventCategoryType in~ ('exchange', 'gmail', 'exchangeserver')
  | where isnotempty(SrcFileName)
  | extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)
  | where file_type in~ (f_types)
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let f_types = dynamic(['ps1', 'bat', 'scr', 'sh', 'exe', 'js', 'lnk']);

Stage 1: source

TrendMicroCAS

Stage 2: where

| where EventCategoryType in~ ('exchange', 'gmail', 'exchangeserver')

Stage 3: where

| where isnotempty(SrcFileName)

Stage 4: extend

| extend file_type = extract(@'\.(\w+)$', 1, SrcFileName)

Stage 5: where

| where file_type in~ (f_types)

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventCategoryTypein
  • exchange corpus 2 (kusto 2)
  • exchangeserver corpus 2 (kusto 2)
  • gmail corpus 2 (kusto 2)
field:"EventCategoryType" kind:in
SrcFileNameis_not_null
  • (no value, null check)
field:"SrcFileName" kind:is_not_null
file_typein
  • bat
  • exe
  • js
  • lnk
  • ps1
  • scr
  • sh
field:"file_type" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
file_typeextend
AccountCustomEntityextend

Back to all Kusto rules