Detection rules › Kusto
Kusto rules: 0d
AWSCloudTrail - Full Admin policy created and then attached to Roles, Users or Groups
#Identifies creation of IAM policies that grant full administrative access and subsequent attachment to a role, user, or group. This sequence can be used to elevate a low-privilege identity to administrative access and should be investigated immediately. AWS IAM Policy Grammar: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html and AWS IAM API at https://docs.aws.amazon.com/IAM/latest/APIReference/API_Operations.html
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- ASL AWS Create Policy Version to allow all resources (Splunk)
- AWS IAM CompromisedKeyQuarantine Policy Attached to User (Elastic)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - CloudFormation policy created then used for privilege escalation (Kusto)
- AWSCloudTrail - Created CRUD S3 policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD KMS policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation (Kusto)
Rule Body
id: 826bb2f8-7894-4785-9a6b-a8a855d8366f
name: AWSCloudTrail - Full Admin policy created and then attached to Roles, Users or Groups
description: |
Identifies creation of IAM policies that grant full administrative access and subsequent attachment to a role, user, or group. This sequence can be used to elevate a low-privilege identity to administrative access and should be investigated immediately.
AWS IAM Policy Grammar: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
and AWS IAM API at https://docs.aws.amazon.com/IAM/latest/APIReference/API_Operations.html
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AWSS3
dataTypes:
- AWSCloudTrail
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1484
query: |
let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;
// Creating Master table with all the events to use with materialize for better performance
let EventInfo = AWSCloudTrail
| where TimeGenerated >= ago(lookback)
| where EventName in (EventNameList) or EventName in (createPolicy)
| 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]), "");
//Checking for Policy creation event with Full Admin Privileges since lookback period.
let FullAdminPolicyEvents = materialize( EventInfo
| where TimeGenerated >= ago(lookback)
| where EventName in (createPolicy)
| 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)
| mvexpand Action
| extend Action = tostring(Action)
| where Effect =~ "Allow" and Action == "*" and Resource == "*"
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
| extend UserIdentityUserName = iff(isnotempty(UserIdentityUserName), UserIdentityUserName, tostring(split(UserIdentityArn,'/')[-1]))
| project-rename StartTime = TimeGenerated );
let PolicyAttach = materialize( EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName, UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType", UserIdentityType, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn, "SourceIpAddress", SourceIpAddress)
| project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
);
// Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
// These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
FullAdminPolicyEvents
| join kind=leftouter
(
PolicyAttach
)
on PolicyName
| project-away PolicyName1
| extend timestamp = StartTime
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
RecipientAccountId: RecipientAccountId
UserIdentityArn: UserIdentityArn
alertDetailsOverride:
alertDisplayNameFormat: 'AWS Full Admin privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.5
kind: Scheduled
Stages and Predicates
Parameters
let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;
let EventInfo and let FullAdminPolicyEvents are inlined into the numbered stages below.
Let Binding: PolicyAttach
let PolicyAttach = materialize( EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName, UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType", UserIdentityType, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn, "SourceIpAddress", SourceIpAddress)
| project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
);
Stages 1 to 16 define let FullAdminPolicyEvents as the rule's main pipeline source. Stages 17 to 20 use it.
Stage 1: source
AWSCloudTrail
Stage 2: where
| where TimeGenerated >= ago(lookback)
Stage 3: where
| where EventName in (EventNameList) or EventName in (createPolicy)
Stage 4: 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 5: where
| where TimeGenerated >= ago(lookback)
Stage 6: where
| where EventName in (createPolicy)
Stage 7: extend
| extend PolicyName = tostring(parse_json(RequestParameters).policyName)
Stage 8: extend
| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
Stage 9: mv-expand
| mvexpand Statement
Stage 10: extend
| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource)
Stage 11: mv-expand
| mvexpand Action
Stage 12: extend
| extend Action = tostring(Action)
Stage 13: where
| where Effect =~ "Allow" and Action == "*" and Resource == "*"
Stage 14: distinct
| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, UserIdentityUserName, RecipientAccountId, AccountName, AccountUPNSuffix
Stage 15: extend
| extend UserIdentityUserName = iff(isnotempty(UserIdentityUserName), UserIdentityUserName, tostring(split(UserIdentityArn,'/')[-1]))
UserIdentityUserName =isnotempty(UserIdentityUserName)UserIdentityUserNametostring(split(UserIdentityArn, '/')[(- 1)])Stage 16: project-rename
| project-rename StartTime = TimeGenerated
Stage 17: join
FullAdminPolicyEvents
| join kind=leftouter
(
PolicyAttach
)
on PolicyName
Stage 18: project-away
project-away PolicyName1
Stage 19: extend
extend timestamp
Stage 20: summarize aggregation inside the join branch
summarize by EventSource, EventName, UserIdentityType, UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq value:"*" |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | summarize |
AccountUPNSuffix | summarize |
EventName | summarize |
EventSource | summarize |
PolicyName | summarize |
RecipientAccountId | summarize |
SourceIpAddress | summarize |
UserIdentityArn | summarize |
UserIdentityType | summarize |
Cloud Gateway Settings Updated
#Detects when cloud gateway settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.
Rule Body
id: 9f0d4171-9a9f-42e4-acbf-9093dd4203de
name: Cloud Gateway Settings Updated
description: Detects when cloud gateway settings are updated in Veeam Backup & Replication.
This might indicate configuration changes that require review.
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 == 24131\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 24131
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"24131" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Conditional Access - A Conditional Access policy was updated
#A Conditional Access policy was updated in Entra ID.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Update conditional access policy |
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure MFA Disabled (Panther)
- Azure Policy Violation Detected (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
Rule Body
id: ccca6b88-a7b6-41c9-9be2-fc3daeb65b26
name: Conditional Access - A Conditional Access policy was updated
version: 1.0.1
kind: Scheduled
description: A Conditional Access policy was updated in Entra ID.
severity: Informational
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |+
// A Conditional Access policy was updated.
AuditLogs
| where OperationName == "Update conditional access policy"
| extend
policy = tostring(TargetResources[0].displayName),
modifiedBy = tostring(InitiatedBy.user.userPrincipalName),
oldPolicy = tostring(TargetResources[0].modifiedProperties[0].oldValue),
newPolicy = tostring(TargetResources[0].modifiedProperties[0].newValue)
| extend
accountName = tostring(split(modifiedBy, "@")[0]),
upnSuffix = tostring(split(modifiedBy, "@")[1])
| project
TimeGenerated,
OperationName,
policy,
modifiedBy,
accountName,
upnSuffix,
oldPolicy,
newPolicy,
Result
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 == "Update conditional access policy"
Stage 3: extend
| extend
policy = tostring(TargetResources[0].displayName),
modifiedBy = tostring(InitiatedBy.user.userPrincipalName),
oldPolicy = tostring(TargetResources[0].modifiedProperties[0].oldValue),
newPolicy = tostring(TargetResources[0].modifiedProperties[0].newValue)
Stage 4: extend
| extend
accountName = tostring(split(modifiedBy, "@")[0]),
upnSuffix = tostring(split(modifiedBy, "@")[1])
Stage 5: project
| project
TimeGenerated,
OperationName,
policy,
modifiedBy,
accountName,
upnSuffix,
oldPolicy,
newPolicy,
Result
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Update conditional access policy" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
OperationName | project |
Result | project |
TimeGenerated | project |
accountName | project |
modifiedBy | project |
newPolicy | project |
oldPolicy | project |
policy | project |
upnSuffix | project |
Create Incident for XDR Alerts
#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.
'This Query creates an incident based on Trend Vision One Workbench Alerts and maps the impacted entities for Microsoft Sentinel usage.'
Rule Body
id: 0febd8cc-1b8d-45ed-87b3-e1e8a57d14cd
name: Create Incident for XDR Alerts
description: |
'This Query creates an incident based on Trend Vision One Workbench Alerts and maps the impacted entities for Microsoft Sentinel usage.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: TrendMicroXDR
dataTypes:
- TrendMicro_XDR_WORKBENCH_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
relevantTechniques:
suppressionDuration: 5h
suppressionEnabled: false
query: |
TrendMicro_XDR_WORKBENCH_CL
| extend Severity = case(severity_s == "low", "Informational",
severity_s == "medium", "Low",
severity_s == "high", "Medium",
"High"
)
| extend
UserAccountName_s = todynamic(column_ifexists("UserAccountName_s", "[]")),
UserAccountNTDomain_s = todynamic(column_ifexists("UserAccountNTDomain_s", "[]")),
FileName_s = todynamic(column_ifexists("FileName_s", "[]")),
FileDirectory_s = todynamic(column_ifexists("FileDirectory_s", "[]")),
ProcessCommandLine_s = todynamic(column_ifexists("ProcessCommandLine_s", "[]")),
RegistryKey_s = todynamic(column_ifexists("RegistryKey_s", "[]")),
RegistryValue_s = todynamic(column_ifexists("RegistryValue_s", "[]")),
RegistryValueName_s = todynamic(column_ifexists("RegistryValueName_s", "[]"))
alertRuleTemplateName: null
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5m
matchingMethod: Selected
groupByCustomDetails:
- WorkbenchID
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserAccountName_s
- identifier: NTDomain
columnName: UserAccountNTDomain_s
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileName_s
- identifier: Directory
columnName: FileDirectory_s
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: ProcessCommandLine_s
- entityType: RegistryKey
fieldMappings:
- identifier: Key
columnName: RegistryKey_s
- entityType: RegistryValue
fieldMappings:
- identifier: Name
columnName: ProcessCommandLine_s
- identifier: Value
columnName: RegistryValue_s
alertDetailsOverride:
alertDisplayNameFormat: '{{workbenchName_s}}'
alertDescriptionFormat: '{{description_s}}'
alertSeverityColumnName: Severity
customDetails:
XDRCustomerID: xdrCustomerID_g
WorkbenchID: workbenchId_s
WorkbenchName: workbenchName_s
WorkbenchLink: workbenchLink_s
PriorityScore: priorityScore_d
CreatedAt: createdTime_t
Severity: severity_s
ImpactScopeSummary: impactScope_Summary_s
Provider: alertProvider_s
eventGroupingSettings:
aggregationKind: AlertPerResult
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
TrendMicro_XDR_WORKBENCH_CL
Stage 2: extend
| extend Severity = case(severity_s == "low", "Informational",
severity_s == "medium", "Low",
severity_s == "high", "Medium",
"High"
)
Severity =severity_s == "low""Informational"severity_s == "medium""Low"severity_s == "high""Medium""High"Stage 3: extend
| extend
UserAccountName_s = todynamic(column_ifexists("UserAccountName_s", "[]")),
UserAccountNTDomain_s = todynamic(column_ifexists("UserAccountNTDomain_s", "[]")),
FileName_s = todynamic(column_ifexists("FileName_s", "[]")),
FileDirectory_s = todynamic(column_ifexists("FileDirectory_s", "[]")),
ProcessCommandLine_s = todynamic(column_ifexists("ProcessCommandLine_s", "[]")),
RegistryKey_s = todynamic(column_ifexists("RegistryKey_s", "[]")),
RegistryValue_s = todynamic(column_ifexists("RegistryValue_s", "[]")),
RegistryValueName_s = todynamic(column_ifexists("RegistryValueName_s", "[]"))
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Severity | extend |
FileDirectory_s | extend |
FileName_s | extend |
ProcessCommandLine_s | extend |
RegistryKey_s | extend |
RegistryValueName_s | extend |
RegistryValue_s | extend |
UserAccountNTDomain_s | extend |
UserAccountName_s | extend |
Dataverse - New Dataverse application user activity type
#Identifies new or previously unseen activity types associated with Dataverse application (non-interactive) user.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Credential Access | |
| Execution |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: 5c768e7d-7e5e-4d57-80d4-3f50c96fbf70
kind: Scheduled
name: Dataverse - New Dataverse application user activity type
description: Identifies new or previously unseen activity types associated with Dataverse
application (non-interactive) user.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- Execution
- PrivilegeEscalation
relevantTechniques:
- T1635
- T0871
- T1078
query: |
let query_frequency = 1h;
let query_lookback = 14d;
let app_user_regex = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\.com$";
let guid_regex = "([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})";
let application_users = DataverseActivity
| where UserId !endswith "@onmicrosoft.com" and UserId != "Unknown"
| summarize by UserId
| where split(UserId, "@")[1] matches regex app_user_regex;
let historical_app_activity = application_users
| join kind = inner (
DataverseActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| summarize by UserId, EntityName, Message, InstanceUrl)
on
UserId;
let current_activity = application_users
| join kind= inner (
DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| summarize by UserId, EntityName, Message, InstanceUrl)
on
UserId;
current_activity
| join kind = leftanti (historical_app_activity) on UserId, Message, EntityName, InstanceUrl
| summarize NewActivities = make_set(strcat(Message, " ", EntityName), 1000) by UserId, InstanceUrl
| extend
AadUserId = extract(guid_regex, 1, tostring(split(UserId, "@")[0])),
CloudAppId = int(32780)
| project
UserId,
NewActivities,
InstanceUrl,
AadUserId,
CloudAppId
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: AadUserId
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - Unusual non-interactive account activity in
{{InstanceUrl}} '
alertDescriptionFormat: '{{UserId}} generated new activities in {{InstanceUrl}}
which had not been seen previously in the Dataverse.'
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 1h;
let query_lookback = 14d;
let app_user_regex = "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\.com$";
let guid_regex = "([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12})";
let application_users and let current_activity are inlined into the numbered stages below.
Let Binding: historical_app_activity
let historical_app_activity = application_users
| join kind = inner (
DataverseActivity
| where TimeGenerated between(ago(query_lookback) .. ago(query_frequency))
| summarize by UserId, EntityName, Message, InstanceUrl)
on
UserId;
Stages 1 to 5 define let current_activity as the rule's main pipeline source. Stages 6 to 9 use it.
Stage 1: source
DataverseActivity
Stage 2: where
where not (UserId endswith "@onmicrosoft.com") and UserId != "Unknown"
Stage 3: summarize
summarize by UserId
Stage 4: where
where /* macro: (split(UserId, "@")[1] matches regex "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\\.com$") */
Stage 5: join
| join kind= inner (
DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| summarize by UserId, EntityName, Message, InstanceUrl)
on
UserId
Stage 6: join (negated)
join kind=leftanti (historical_app_activity) on UserId, Message, EntityName, InstanceUrl
Stage 7: summarize
summarize NewActivities by UserId, InstanceUrl
Stage 8: extend
extend AadUserId, CloudAppId
Stage 9: project
project AadUserId, CloudAppId, InstanceUrl, NewActivities, UserId
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
UserId | ends_with |
| field:"UserId" kind:ends_with value:"@onmicrosoft.com" |
UserId | ne |
| field:"user" kind:ne value:"Unknown" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
UserId | ends_with | @onmicrosoft.com | excludes:UserId field:"UserId" value:"@onmicrosoft.com" |
UserId | ne | Unknown | excludes:UserId field:"UserId" value:"Unknown" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AadUserId | project |
CloudAppId | project |
InstanceUrl | project |
NewActivities | project |
UserId | project |
Detect potential file enumeration activity (ASIM Web Session)
#'This detection method identifies potential cases of file enumeration activity. The query is designed to identify client sources that generate multiple requests resulting in 404 error codes'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Discovery | |
| Command & Control |
Rule Body
id: b3731ce1-1f04-47c4-95c2-9827408c4375
name: Detect potential file enumeration activity (ASIM Web Session)
description: |
'This detection method identifies potential cases of file enumeration activity. The query is designed to identify client sources that generate multiple requests resulting in 404 error codes'
severity: Medium
status: Available
tags:
- Schema: WebSession
SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
- CommandAndControl
- CredentialAccess
relevantTechniques:
- T1083
- T1071
- T1110
query: |
// HTTP response status codes indicate whether a specific HTTP request has been successfully completed.
// Please refer this for more details: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
let HTTPErrorCode=dynamic(["404"]);
let threshold = 10; // You can update this threshold to the value that suites your environment
// let FileNotFoundRequests =
_Im_WebSession (starttime=ago(1h))
| where EventResultDetails in~ (HTTPErrorCode)
// Filter the logs to include only HTTP GET requests with an HTTP status code of 404 and '/' in the URL
| where HttpRequestMethod =~ "GET" and Url contains "/"
| summarize
RequestCount = count(),
FileCount=dcount(Url),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated),
RequestURLs = make_set(Url, 100),
DestinationIPList=make_set(DstIpAddr, 100)
by SrcIpAddr, SrcUsername, SrcHostname, DstHostname
| where RequestCount > threshold // Adjust the threshold as per your requirements
| 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: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DstHostname
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SrcHostname
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
RequestCount: RequestCount
RequestURLs: RequestURLs
FileCount: FileCount
EventStartTime: EventStartTime
EventEndTime: EventEndTime
DestinationIPList: DestinationIPList
alertDetailsOverride:
alertDisplayNameFormat: "User '{{SrcUsername}}' with IP '{{SrcIpAddr}}' has been observed with performing file enumeration activity"
alertDescriptionFormat: "User generated multiple requests '{{RequestCount}}' that has resulted in error code '404', suggesting the possibility of file enumeration activity. It's important to investigate the source and patterns of these extensive 404 errors to identify potential security threats. Details about this error code could be found [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)"
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let HTTPErrorCode = dynamic(["404"]);
let threshold = 10;
Stage 1: source
_Im_WebSession (starttime=ago(1h))
Stage 2: where
| where EventResultDetails in~ (HTTPErrorCode)
Stage 3: where
| where HttpRequestMethod =~ "GET" and Url contains "/"
Stage 4: summarize
| summarize
RequestCount = count(),
FileCount=dcount(Url),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated),
RequestURLs = make_set(Url, 100),
DestinationIPList=make_set(DstIpAddr, 100)
by SrcIpAddr, SrcUsername, SrcHostname, DstHostname
Stage 5: where
| where RequestCount > threshold
Stage 6: 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.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationIPList | summarize |
DstHostname | summarize |
EventEndTime | summarize |
EventStartTime | summarize |
FileCount | summarize |
RequestCount | summarize |
RequestURLs | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
Name | extend |
UPNSuffix | extend |
External Repository Settings Updated
#Detects when external repository settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.
Rule Body
id: d035188d-d856-4a74-8226-f3bdf65033c4
name: External Repository Settings Updated
description: Detects when external repository settings are updated in Veeam Backup
& Replication. This might indicate configuration changes that require review.
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 == 32100\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 32100
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"32100" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Fortinet - Beacon pattern detected
#'Identifies patterns in the time deltas of contacts between internal and external IPs in Fortinet network data that are consistent with beaconing. Accounts for randomness (jitter) and seasonality such as working hours that may have been introduced into the beacon pattern. The lookback is set to 1d, the minimum granularity in time deltas is set to 60 seconds and the minimum number of beacons required to emit a detection is set to 4. Increase the lookback period to capture beacons with larger periodicities. The jitter tolerance is set to 0.2 - This means we account for an overall 20% deviation from the infered beacon periodicity. Seasonality is dealt with automatically using series_outliers. Note: In large environments it may be necessary to reduce the lookback period to get fast query times.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 3255ec41-6bd6-4f35-84b1-c032b18bbfcb
name: Fortinet - Beacon pattern detected
description: |
'Identifies patterns in the time deltas of contacts between internal and external IPs in Fortinet network data that are consistent with beaconing.
Accounts for randomness (jitter) and seasonality such as working hours that may have been introduced into the beacon pattern.
The lookback is set to 1d, the minimum granularity in time deltas is set to 60 seconds and the minimum number of beacons required to emit a detection is set to 4.
Increase the lookback period to capture beacons with larger periodicities.
The jitter tolerance is set to 0.2 - This means we account for an overall 20% deviation from the infered beacon periodicity. Seasonality is dealt with automatically using series_outliers.
Note: In large environments it may be necessary to reduce the lookback period to get fast query times.'
severity: Low
requiredDataConnectors:
- connectorId: Fortinet
dataTypes:
- CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
- T1571
query: |
let starttime = 1d;
let TimeDeltaThresholdInSeconds = 60; // we ignore beacons diffs that fall below this threshold
let TotalBeaconsThreshold = 4; // minimum number of beacons required in a session to surface a row
let JitterTolerance = 0.2; // tolerance to jitter, e.g. - 0.2 = 20% jitter is tolerated either side of the periodicity
CommonSecurityLog
| where DeviceVendor == "Fortinet"
| where TimeGenerated > ago(starttime)
// eliminate bad data
| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
// filter out deny, close, rst and SNMP to reduce data volume
| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
// map input fields
| project TimeGenerated , SourceIP, DestinationIP, DestinationPort, ReceivedBytes, SentBytes, DeviceAction
// where destination IPs are public
| where ipv4_is_private(DestinationIP) == false
// sort into source->destination 'sessions'
| sort by SourceIP asc, DestinationIP asc, DestinationPort asc, TimeGenerated asc
| serialize
// time diff the contact times between source and destination to get a list of deltas
| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(SourceIP, 1), nextDestIP = next(DestinationIP, 1), nextDestPort = next(DestinationPort, 1)
| extend TimeDeltainSeconds = datetime_diff("second",nextTimeGenerated,TimeGenerated)
| where SourceIP == nextSourceIP and DestinationIP == nextDestIP and DestinationPort == nextDestPort
// remove small time deltas below the set threshold
| where TimeDeltainSeconds > TimeDeltaThresholdInSeconds
// summarize the deltas by source->destination
| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), sum(ReceivedBytes), sum(SentBytes), makelist(TimeDeltainSeconds), makeset(DeviceAction) by SourceIP, DestinationIP, DestinationPort
// get some statistical properties of the delta distribution and smooth any outliers (e.g. laptop shut overnight, working hours)
| extend series_stats(list_TimeDeltainSeconds), outliers=series_outliers(list_TimeDeltainSeconds)
// expand the deltas and the outliers
| mvexpand list_TimeDeltainSeconds to typeof(double), outliers to typeof(double)
// replace outliers with the average of the distribution
| extend list_TimeDeltainSeconds_normalized=iff(outliers > 1.5 or outliers < -1.5, series_stats_list_TimeDeltainSeconds_avg , list_TimeDeltainSeconds)
// summarize with the smoothed distribution
| summarize BeaconCount=count(), makelist(list_TimeDeltainSeconds), list_TimeDeltainSeconds_normalized=makelist(list_TimeDeltainSeconds_normalized), makeset(set_DeviceAction) by StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, sum_ReceivedBytes, sum_SentBytes
// get stats on the smoothed distribution
| extend series_stats(list_TimeDeltainSeconds_normalized)
// match jitter tolerance on smoothed distrib
| extend MaxJitter = (series_stats_list_TimeDeltainSeconds_normalized_avg*JitterTolerance)
| where series_stats_list_TimeDeltainSeconds_normalized_stdev < MaxJitter
// where the minimum beacon threshold is satisfied and there was some data transfer
| where BeaconCount > TotalBeaconsThreshold and (sum_SentBytes > 0 or sum_ReceivedBytes > 0)
// final projection
| project StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, BeaconCount, TimeDeltasInSeconds=list_list_TimeDeltainSeconds, Periodicity=series_stats_list_TimeDeltainSeconds_normalized_avg, ReceivedBytes=sum_ReceivedBytes, SentBytes=sum_SentBytes, Actions=set_set_DeviceAction
// where periodicity is order of magnitude larger than time delta threshold (eliminates FPs whose periodicity is close to the values we ignored)
| where Periodicity >= (10*TimeDeltaThresholdInSeconds)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DestinationIP
version: 1.0.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Network" ]
Stages and Predicates
Parameters
let starttime = 1d;
let TimeDeltaThresholdInSeconds = 60;
let TotalBeaconsThreshold = 4;
let JitterTolerance = 0.2;
Stage 1: source
CommonSecurityLog
Stage 2: where
| where DeviceVendor == "Fortinet"
Stage 3: where
| where TimeGenerated > ago(starttime)
Stage 4: where
| where isnotempty(SourceIP) and isnotempty(DestinationIP) and SourceIP != "0.0.0.0"
Stage 5: where
| where DeviceAction !in ("close", "client-rst", "server-rst", "deny") and DestinationPort != 161
Stage 6: project
| project TimeGenerated , SourceIP, DestinationIP, DestinationPort, ReceivedBytes, SentBytes, DeviceAction
Stage 7: where
| where ipv4_is_private(DestinationIP) == false
Stage 8: sort
| sort by SourceIP asc, DestinationIP asc, DestinationPort asc, TimeGenerated asc
Stage 9: kusto:serialize
| serialize
Stage 10: extend
| extend nextTimeGenerated = next(TimeGenerated, 1), nextSourceIP = next(SourceIP, 1), nextDestIP = next(DestinationIP, 1), nextDestPort = next(DestinationPort, 1)
Stage 11: extend
| extend TimeDeltainSeconds = datetime_diff("second",nextTimeGenerated,TimeGenerated)
Stage 12: where
| where SourceIP == nextSourceIP and DestinationIP == nextDestIP and DestinationPort == nextDestPort
Stage 13: where
| where TimeDeltainSeconds > TimeDeltaThresholdInSeconds
Stage 14: summarize
| summarize count(), StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), sum(ReceivedBytes), sum(SentBytes), makelist(TimeDeltainSeconds), makeset(DeviceAction) by SourceIP, DestinationIP, DestinationPort
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 15: extend
| extend series_stats(list_TimeDeltainSeconds), outliers=series_outliers(list_TimeDeltainSeconds)
Stage 16: mv-expand
| mvexpand list_TimeDeltainSeconds to typeof(double), outliers to typeof(double)
Stage 17: extend
| extend list_TimeDeltainSeconds_normalized=iff(outliers > 1.5 or outliers < -1.5, series_stats_list_TimeDeltainSeconds_avg , list_TimeDeltainSeconds)
list_TimeDeltainSeconds_normalized =(outliers > 1.5 or outliers < -1.5)series_stats_list_TimeDeltainSeconds_avglist_TimeDeltainSecondsStage 18: summarize
| summarize BeaconCount=count(), makelist(list_TimeDeltainSeconds), list_TimeDeltainSeconds_normalized=makelist(list_TimeDeltainSeconds_normalized), makeset(set_DeviceAction) by StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, sum_ReceivedBytes, sum_SentBytes
Stage 19: extend
| extend series_stats(list_TimeDeltainSeconds_normalized)
Stage 20: extend
| extend MaxJitter = (series_stats_list_TimeDeltainSeconds_normalized_avg*JitterTolerance)
Stage 21: where
| where series_stats_list_TimeDeltainSeconds_normalized_stdev < MaxJitter
Stage 22: where
| where BeaconCount > TotalBeaconsThreshold and (sum_SentBytes > 0 or sum_ReceivedBytes > 0)
Stage 23: project
| project StartTime, EndTime, SourceIP, DestinationIP, DestinationPort, BeaconCount, TimeDeltasInSeconds=list_list_TimeDeltainSeconds, Periodicity=series_stats_list_TimeDeltainSeconds_normalized_avg, ReceivedBytes=sum_ReceivedBytes, SentBytes=sum_SentBytes, Actions=set_set_DeviceAction
Stage 24: where
| where Periodicity >= (10*TimeDeltaThresholdInSeconds)
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DeviceAction | in | client-rst, close, deny, server-rst | excludes:DeviceAction |
DestinationIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:DestinationIP |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Actions | project |
BeaconCount | project |
DestinationIP | project |
DestinationPort | project |
EndTime | project |
Periodicity | project |
ReceivedBytes | project |
SentBytes | project |
SourceIP | project |
StartTime | project |
TimeDeltasInSeconds | project |
Global VM Exclusions Changed
#Detects when global VM exclusions are updated in Veeam Backup & Replication.
Rule Body
id: 2034c441-2850-4167-80ed-b4438155836d
name: Global VM Exclusions Changed
description: Detects when global VM exclusions are updated in Veeam Backup & Replication.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40600\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 40600
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"40600" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Imperva - Forbidden HTTP request method in request
#'Detects connections with unexpected HTTP request method.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 7ebc9e24-319c-4786-9151-c898240463bc
name: Imperva - Forbidden HTTP request method in request
description: |
'Detects connections with unexpected HTTP request method.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: ImpervaWAFCloudAPI
dataTypes:
- ImpervaWAFCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let bl_http_methods = dynamic(['PUT', 'HEAD', 'OPTIONS', 'TRACE', 'POST']);
ImpervaWAFCloud
| where HttpRequestMethod in~ (bl_http_methods)
| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = UrlOriginal
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let bl_http_methods = dynamic(['PUT', 'HEAD', 'OPTIONS', 'TRACE', 'POST']);
Stage 1: source
ImpervaWAFCloud
Stage 2: where
| where HttpRequestMethod in~ (bl_http_methods)
Stage 3: extend
| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpRequestMethod | in |
| field:"HttpRequestMethod" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
UrlCustomEntity | extend |
Imperva - Malicious user agent
#'Detects requests containing known malicious user agent strings.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 905794a9-bc46-42b9-974d-5a2dd58110c5
name: Imperva - Malicious user agent
description: |
'Detects requests containing known malicious user agent strings.'
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 HttpUserAgentOriginal has_any ('Nikto', 'hydra', 'advanced email extractor', 'BFAC', 'brutus', 'cgichk', 'cisco-torch', 'scanner', 'datacha0s', 'dirbuster', 'grabber', 'havij', 'internet ninja', 'masscan', 'morfeus', 'mysqloit', 'n-stealth', 'nessus', 'netsparker', 'nmap nse', 'nmap scripting engine', 'nmap-nse', 'nsauditor', 'openvas', 'pangolin', 'qualys was', 'security scan', 'springenwerk', 'sql power injector', 'sqlmap', 'sqlninja', 'w3af.sf.net', 'w3af.sourceforge.net', 'w3af.org', 'webbandit', 'webinspect', 'webvulnscan', 'xmlrpc exploit', 'WPScan', 'XSpider', 'Webster', 'fantomCrew', 'fantomBrowser')
| summarize count() by SrcIpAddr, bin(TimeGenerated, 5m)
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
ImpervaWAFCloud
Stage 2: where
| where HttpUserAgentOriginal has_any ('Nikto', 'hydra', 'advanced email extractor', 'BFAC', 'brutus', 'cgichk', 'cisco-torch', 'scanner', 'datacha0s', 'dirbuster', 'grabber', 'havij', 'internet ninja', 'masscan', 'morfeus', 'mysqloit', 'n-stealth', 'nessus', 'netsparker', 'nmap nse', 'nmap scripting engine', 'nmap-nse', 'nsauditor', 'openvas', 'pangolin', 'qualys was', 'security scan', 'springenwerk', 'sql power injector', 'sqlmap', 'sqlninja', 'w3af.sf.net', 'w3af.sourceforge.net', 'w3af.org', 'webbandit', 'webinspect', 'webvulnscan', 'xmlrpc exploit', 'WPScan', 'XSpider', 'Webster', 'fantomCrew', 'fantomBrowser')
Stage 3: summarize
| summarize count() by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 4: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpUserAgentOriginal | match |
| field:"HttpUserAgentOriginal" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
IPCustomEntity | extend |
Pathlock TDnR - Generic SAP Change Documents
#Detects generic SAP change document events across all object classes, forwarded by Pathlock Threat Detection and Response. This broad datasource captures cross-functional object modifications that may indicate unauthorized data tampering or configuration changes.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment | |
| Impact |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c09
name: Pathlock TDnR - Generic SAP Change Documents
kind: Scheduled
description: >-
Detects generic SAP change document events across all object classes, forwarded by Pathlock Threat
Detection and Response. This broad datasource captures cross-functional object modifications
that may indicate unauthorized data tampering or configuration changes.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
- DefenseEvasion
relevantTechniques:
- T1565
- T1562
query: |
Pathlock_TDnR_CL
| where DataSource == "CHANGEDOC_GENERIC"
| 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 == "CHANGEDOC_GENERIC"
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"CHANGEDOC_GENERIC" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
StealthTalk - After hours work
#Identifies systematic off-hours activity for a single StealthTalk user - repeated authentications outside the user's configured working hours OR on weekends, observed across at least two distinct calendar days within a 48-hour window. The pattern is a common indicator of credential misuse, insider threat, or compromise of the account by an attacker operating in a different timezone. An "off-hours event" is one where IsWeekend=true OR DeviationMinutes >= 180 (i.e. >= 3 hours after the configured working-hours end). Three or more such events on at least two distinct days are required for an incident to fire - a single late-evening login is not enough.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Stealth |
Rule Body
id: e3a8b2f1-5c7d-4d89-9b6e-0f1a2c3d4e5f
name: StealthTalk - After hours work
description: |
Identifies systematic off-hours activity for a single StealthTalk user - repeated authentications
outside the user's configured working hours OR on weekends, observed across at least two
distinct calendar days within a 48-hour window. The pattern is a common indicator of credential
misuse, insider threat, or compromise of the account by an attacker operating in a different
timezone.
An "off-hours event" is one where IsWeekend=true OR DeviationMinutes >= 180 (i.e. >= 3 hours
after the configured working-hours end). Three or more such events on at least two distinct
days are required for an incident to fire - a single late-evening login is not enough.
severity: Low
requiredDataConnectors:
- connectorId: StealthTalkAnomalousAuth
dataTypes:
- StealthTalkAnomalousAuth_CL
queryFrequency: 1h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- DefenseEvasion
- Persistence
relevantTechniques:
- T1078
query: |
let LookbackPeriod = 48h;
let MinAttempts = 3;
let MinDistinctDays = 2;
let OffHoursThreshold = 180;
StealthTalkAnomalousAuth_CL
| where TimeGenerated >= ago(LookbackPeriod)
| where EventType == "OffHoursLogin"
| where IsWeekend == true or DeviationMinutes >= OffHoursThreshold
| summarize
AttemptCount = count(),
DistinctDays = dcount(startofday(TimeGenerated)),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
DeviceIds = make_set(DeviceId),
MaxDeviation = max(DeviationMinutes),
WeekendsCount = countif(IsWeekend == true),
AppVersions = make_set(AppVersion)
by UserId
| where AttemptCount >= MinAttempts and DistinctDays >= MinDistinctDays
| extend
AlertName = "AfterHoursWork",
AlertDetails = strcat(
"User ", UserId,
" performed ", AttemptCount, " off-hours logins",
" across ", DistinctDays, " distinct days.",
" Max deviation from working hours: ", MaxDeviation, " min.",
" Weekend logins: ", WeekendsCount, "."
)
| project
TimeGenerated = LastSeen,
UserId, AttemptCount, DistinctDays, MaxDeviation,
WeekendsCount, DeviceIds, AppVersions, FirstSeen, LastSeen,
AlertName, AlertDetails
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: UserId
customDetails:
AttemptCount: AttemptCount
DistinctDays: DistinctDays
MaxDeviation: MaxDeviation
WeekendsCount: WeekendsCount
FirstSeen: FirstSeen
LastSeen: LastSeen
alertDetailsOverride:
alertDisplayNameFormat: 'StealthTalk: After-Hours Work - {{UserId}} ({{AttemptCount}} events / {{DistinctDays}} days)'
alertDescriptionFormat: '{{AlertDetails}}'
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: Selected
groupByEntities:
- Account
suppressionEnabled: false
suppressionDuration: 5h
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let LookbackPeriod = 48h;
let MinAttempts = 3;
let MinDistinctDays = 2;
let OffHoursThreshold = 180;
Stage 1: source
StealthTalkAnomalousAuth_CL
Stage 2: where
| where TimeGenerated >= ago(LookbackPeriod)
Stage 3: where
| where EventType == "OffHoursLogin"
Stage 4: where
| where IsWeekend == true or DeviationMinutes >= OffHoursThreshold
Stage 5: summarize
| summarize
AttemptCount = count(),
DistinctDays = dcount(startofday(TimeGenerated)),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated),
DeviceIds = make_set(DeviceId),
MaxDeviation = max(DeviationMinutes),
WeekendsCount = countif(IsWeekend == true),
AppVersions = make_set(AppVersion)
by UserId
Stage 6: where
| where AttemptCount >= MinAttempts and DistinctDays >= MinDistinctDays
Stage 7: extend
| extend
AlertName = "AfterHoursWork",
AlertDetails = strcat(
"User ", UserId,
" performed ", AttemptCount, " off-hours logins",
" across ", DistinctDays, " distinct days.",
" Max deviation from working hours: ", MaxDeviation, " min.",
" Weekend logins: ", WeekendsCount, "."
)
Stage 8: project
| project
TimeGenerated = LastSeen,
UserId, AttemptCount, DistinctDays, MaxDeviation,
WeekendsCount, DeviceIds, AppVersions, FirstSeen, LastSeen,
AlertName, AlertDetails
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertDetails | project |
AlertName | project |
AppVersions | project |
AttemptCount | project |
DeviceIds | project |
DistinctDays | project |
FirstSeen | project |
LastSeen | project |
MaxDeviation | project |
TimeGenerated | project |
UserId | project |
WeekendsCount | project |
URL Added to Application from Unknown Domain
#Detects a URL being added to an application where the domain is not one that is associated with the tenant. The query uses domains seen in sign in logs to determine if the domain is associated with the tenant. Applications associated with URLs not controlled by the organization can pose a security risk. Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Update application |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule Body
id: 017e095a-94d8-430c-a047-e51a11fb737b
name: URL Added to Application from Unknown Domain
description: |
'Detects a URL being added to an application where the domain is not one that is associated with the tenant.
The query uses domains seen in sign in logs to determine if the domain is associated with the tenant.
Applications associated with URLs not controlled by the organization can pose a security risk.
Ref: https://learn.microsoft.com/en-gb/entra/architecture/security-operations-applications#application-configuration-changes'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
let domains =
SigninLogs
| where ResultType == 0
| extend domain = split(UserPrincipalName, "@")[1]
| extend domain = tostring(split(UserPrincipalName, "@")[1])
| summarize by tolower(tostring(domain));
AuditLogs
| where Category =~ "ApplicationManagement"
| where Result =~ "success"
| where OperationName =~ 'Update Application'
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
| where TargetResources_modifiedProperties.displayName =~ "AppAddress"
| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue
| where isnotempty(Key) and isnotempty(NewValue)
| project-reorder Key, NewValue, OldValue
| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)
| where array_length(AddedUrls) > 0
| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| 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(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AppDisplayName = tostring(TargetResources.displayName)
| where isnotempty(AddedUrls)
| mv-expand AddedUrls
| extend AddedUrls = trim(@'"', tostring(AddedUrls))
| extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
| where isnotempty(Domain)
| extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])
| where Domain !in (domains)
| project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: AddedUrls
- 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
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Let Binding: domains
let domains = SigninLogs
| where ResultType == 0
| extend domain = split(UserPrincipalName, "@")[1]
| extend domain = tostring(split(UserPrincipalName, "@")[1])
| summarize by tolower(tostring(domain));
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "ApplicationManagement"
Stage 3: where
| where Result =~ "success"
Stage 4: where
| where OperationName =~ 'Update Application'
Stage 5: mv-expand
| mv-expand TargetResources
Stage 6: mv-expand
| mv-expand TargetResources.modifiedProperties
Stage 7: where
| where TargetResources_modifiedProperties.displayName =~ "AppAddress"
Stage 8: extend (3 consecutive steps)
| extend Key = tostring(TargetResources_modifiedProperties.displayName)
| extend NewValue = TargetResources_modifiedProperties.newValue
| extend OldValue = TargetResources_modifiedProperties.oldValue
Stage 9: where
| where isnotempty(Key) and isnotempty(NewValue)
Stage 10: project-reorder
| project-reorder Key, NewValue, OldValue
Stage 11: extend (3 consecutive steps)
| extend NewUrls = extract_all('"Address":([^,]*)', tostring(NewValue))
| extend OldUrls = extract_all('"Address":([^,]*)', tostring(OldValue))
| extend AddedUrls = set_difference(NewUrls, OldUrls)
Stage 12: where
| where array_length(AddedUrls) > 0
Stage 13: extend (8 consecutive steps)
| extend UserAgent = iif(tostring(AdditionalDetails[0].key) == "User-Agent", tostring(AdditionalDetails[0].value), "")
| 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(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AppDisplayName = tostring(TargetResources.displayName)
Stage 14: where
| where isnotempty(AddedUrls)
Stage 15: mv-expand
| mv-expand AddedUrls
Stage 16: extend
| extend AddedUrls = trim(@'"', tostring(AddedUrls))
Stage 17: extend
| extend Domain = extract("^(?:https?:\\/\\/)?(?:[^@\\/\\n]+@)?(?:www\\.)?([^:\\/?\\n]+)/", 1, replace_string(tolower(AddedUrls), '"', ""))
Stage 18: where
| where isnotempty(Domain)
Stage 19: extend
| extend Domain = strcat(split(Domain, ".")[-2], ".", split(Domain, ".")[-1])
Stage 20: where
| where Domain !in (domains)
Stage 21: project-reorder
| project-reorder TimeGenerated, AppDisplayName, AddedUrls, InitiatedBy, UserAgent, InitiatingIPAddress
Stage 22: extend
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AddedUrls | is_not_null | field:"AddedUrls" kind:is_not_null | |
Category | eq |
| field:"Category" kind:eq value:"ApplicationManagement" |
Domain | is_not_null | field:"Domain" kind:is_not_null | |
Key | is_not_null | field:"Key" kind:is_not_null | |
NewValue | is_not_null | field:"Details" kind:is_not_null | |
OperationName | eq |
| field:"OperationName" kind:eq value:"Update Application" |
Result | eq |
| field:"Result" kind:eq value:"success" |
TargetResources_modifiedProperties.displayName | eq |
| field:"TargetResources_modifiedProperties.displayName" kind:eq value:"AppAddress" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Domain | eq | domains | excludes:Domain field:"Domain" value:"domains" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Key | extend |
NewValue | extend |
OldValue | extend |
NewUrls | extend |
OldUrls | extend |
AddedUrls | extend |
UserAgent | extend |
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIPAddress | extend |
InitiatedBy | extend |
AppDisplayName | extend |
Domain | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |