Detection rules › Kusto
Kusto rules: 1o
Admin promotion after Role Management Application Permission Grant
#This rule looks for a service principal being granted the Microsoft Graph RoleManagement.ReadWrite.Directory (application) permission before being used to add an Microsoft Entra ID object or user account to an Admin directory role (i.e. Global Administrators). This is a known attack path that is usually abused when a service principal already has the AppRoleAssignment.ReadWrite.All permission granted. This permission allows an app to manage permission grants for application permissions to any API. A service principal can promote itself or other service principals to admin roles (i.e. Global Administrators). This would be considered a privilege escalation technique. Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions, https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add app role assignment to service principal |
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)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Added Credentials to Existing Application (Sigma)
Rule Body
id: f80d951a-eddc-4171-b9d0-d616bb83efdc
name: Admin promotion after Role Management Application Permission Grant
description: |
'This rule looks for a service principal being granted the Microsoft Graph RoleManagement.ReadWrite.Directory (application) permission before being used to add an Microsoft Entra ID object or user account to an Admin directory role (i.e. Global Administrators).
This is a known attack path that is usually abused when a service principal already has the AppRoleAssignment.ReadWrite.All permission granted. This permission allows an app to manage permission grants for application permissions to any API.
A service principal can promote itself or other service principals to admin roles (i.e. Global Administrators). This would be considered a privilege escalation technique.
Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions, https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http'
severity: High
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- PrivilegeEscalation
- Persistence
relevantTechniques:
- T1098.003
- T1078.004
tags:
- SimuLand
query: |
let query_frequency = 1h;
let query_period = 2h;
AuditLogs
| where TimeGenerated > ago(query_period)
| where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory"
| where OperationName =~ "Add app role assignment to service principal"
| mv-expand TargetResource = TargetResources
| mv-expand modifiedProperty = TargetResource["modifiedProperties"]
| where tostring(modifiedProperty["displayName"]) == "AppRole.Value"
| extend PermissionGrant = tostring(modifiedProperty["newValue"])
| where PermissionGrant has "RoleManagement.ReadWrite.Directory"
| mv-apply modifiedProperty = TargetResource["modifiedProperties"] on (
summarize modifiedProperties = make_bag(
bag_pack(tostring(modifiedProperty["displayName"]),
bag_pack("oldValue", trim(@'[\"\s]+', tostring(modifiedProperty["oldValue"])),
"newValue", trim(@'[\"\s]+', tostring(modifiedProperty["newValue"])))), 100)
)
| project
PermissionGrant_TimeGenerated = TimeGenerated,
PermissionGrant_OperationName = OperationName,
PermissionGrant_Result = Result,
PermissionGrant,
AppDisplayName = tostring(modifiedProperties["ServicePrincipal.DisplayName"]["newValue"]),
AppServicePrincipalId = tostring(modifiedProperties["ServicePrincipal.ObjectID"]["newValue"]),
PermissionGrant_InitiatedBy = InitiatedBy,
PermissionGrant_TargetResources = TargetResources,
PermissionGrant_AdditionalDetails = AdditionalDetails,
PermissionGrant_CorrelationId = CorrelationId
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "RoleManagement" and LoggedByService =~ "Core Directory" and AADOperationType =~ "Assign"
| where isnotempty(InitiatedBy["app"])
| mv-expand TargetResource = TargetResources
| mv-expand modifiedProperty = TargetResource["modifiedProperties"]
| where tostring(modifiedProperty["displayName"]) in ("Role.DisplayName", "RoleDefinition.DisplayName")
| extend RoleAssignment = tostring(modifiedProperty["newValue"])
| where RoleAssignment contains "Admin"
| project
RoleAssignment_TimeGenerated = TimeGenerated,
RoleAssignment_OperationName = OperationName,
RoleAssignment_Result = Result,
RoleAssignment,
TargetType = tostring(TargetResources[0]["type"]),
Target = iff(isnotempty(TargetResources[0]["displayName"]), tostring(TargetResources[0]["displayName"]), tolower(TargetResources[0]["userPrincipalName"])),
TargetId = tostring(TargetResources[0]["id"]),
RoleAssignment_InitiatedBy = InitiatedBy,
RoleAssignment_TargetResources = TargetResources,
RoleAssignment_AdditionalDetails = AdditionalDetails,
RoleAssignment_CorrelationId = CorrelationId,
AppServicePrincipalId = tostring(InitiatedBy["app"]["servicePrincipalId"])
) on AppServicePrincipalId
| where PermissionGrant_TimeGenerated < RoleAssignment_TimeGenerated
| extend
TargetName = tostring(split(Target, "@")[0]),
TargetUPNSuffix = tostring(split(Target, "@")[1])
| project PermissionGrant_TimeGenerated, PermissionGrant_OperationName, PermissionGrant_Result, PermissionGrant, AppDisplayName, AppServicePrincipalId, PermissionGrant_InitiatedBy, PermissionGrant_TargetResources, PermissionGrant_AdditionalDetails, PermissionGrant_CorrelationId,
RoleAssignment_TimeGenerated, RoleAssignment_OperationName, RoleAssignment_Result, RoleAssignment, TargetType, Target, TargetName, TargetUPNSuffix, TargetId, RoleAssignment_InitiatedBy, RoleAssignment_TargetResources, RoleAssignment_AdditionalDetails, RoleAssignment_CorrelationId
| extend PermissionGrant_InitiatingUserPrincipalName = tostring(PermissionGrant_InitiatedBy.user.userPrincipalName)
| extend PermissionGrant_InitiatingAadUserId = tostring(PermissionGrant_InitiatedBy.user.id)
| extend PermissionGrant_InitiatingIpAddress = tostring(iff(isnotempty(PermissionGrant_InitiatedBy.user.ipAddress), PermissionGrant_InitiatedBy.user.ipAddress, PermissionGrant_InitiatedBy.app.ipAddress))
| extend PermissionGrant_InitiatingAccountName = tostring(split(PermissionGrant_InitiatingUserPrincipalName, "@")[0]), PermissionGrant_InitiatingAccountUPNSuffix = tostring(split(PermissionGrant_InitiatingUserPrincipalName, "@")[1])
| extend RoleAssignment_InitiatingUserPrincipalName = tostring(RoleAssignment_InitiatedBy.user.userPrincipalName)
| extend RoleAssignment_InitiatingAadUserId = tostring(RoleAssignment_InitiatedBy.user.id)
| extend RoleAssignment_InitiatingIpAddress = tostring(iff(isnotempty(RoleAssignment_InitiatedBy.user.ipAddress), RoleAssignment_InitiatedBy.user.ipAddress, RoleAssignment_InitiatedBy.app.ipAddress))
| extend RoleAssignment_InitiatingAccountName = tostring(split(RoleAssignment_InitiatingUserPrincipalName, "@")[0]), RoleAssignment_InitiatingAccountUPNSuffix = tostring(split(RoleAssignment_InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AppDisplayName
- identifier: AadUserId
columnName: AppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Target
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: PermissionGrant_InitiatingUserPrincipalName
- identifier: Name
columnName: PermissionGrant_InitiatingAccountName
- identifier: UPNSuffix
columnName: PermissionGrant_InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: PermissionGrant_InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: RoleAssignment_InitiatingUserPrincipalName
- identifier: Name
columnName: RoleAssignment_InitiatingAccountName
- identifier: UPNSuffix
columnName: RoleAssignment_InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: RoleAssignment_InitiatingAadUserId
version: 1.1.0
kind: Scheduled
Stages and Predicates
Parameters
let query_frequency = 1h;
let query_period = 2h;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(query_period)
Stage 3: where
| where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory"
Stage 4: where
| where OperationName =~ "Add app role assignment to service principal"
Stage 5: mv-expand
| mv-expand TargetResource = TargetResources
Stage 6: mv-expand
| mv-expand modifiedProperty = TargetResource["modifiedProperties"]
Stage 7: where
| where tostring(modifiedProperty["displayName"]) == "AppRole.Value"
Stage 8: extend
| extend PermissionGrant = tostring(modifiedProperty["newValue"])
Stage 9: where
| where PermissionGrant has "RoleManagement.ReadWrite.Directory"
Stage 10: kusto:mv-apply
| mv-apply modifiedProperty = TargetResource["modifiedProperties"] on (
summarize modifiedProperties = make_bag(
bag_pack(tostring(modifiedProperty["displayName"]),
bag_pack("oldValue", trim(@'[\"\s]+', tostring(modifiedProperty["oldValue"])),
"newValue", trim(@'[\"\s]+', tostring(modifiedProperty["newValue"])))), 100)
)
Stage 11: project
| project
PermissionGrant_TimeGenerated = TimeGenerated,
PermissionGrant_OperationName = OperationName,
PermissionGrant_Result = Result,
PermissionGrant,
AppDisplayName = tostring(modifiedProperties["ServicePrincipal.DisplayName"]["newValue"]),
AppServicePrincipalId = tostring(modifiedProperties["ServicePrincipal.ObjectID"]["newValue"]),
PermissionGrant_InitiatedBy = InitiatedBy,
PermissionGrant_TargetResources = TargetResources,
PermissionGrant_AdditionalDetails = AdditionalDetails,
PermissionGrant_CorrelationId = CorrelationId
Stage 12: join
| join kind=inner (
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "RoleManagement" and LoggedByService =~ "Core Directory" and AADOperationType =~ "Assign"
| where isnotempty(InitiatedBy["app"])
| mv-expand TargetResource = TargetResources
| mv-expand modifiedProperty = TargetResource["modifiedProperties"]
| where tostring(modifiedProperty["displayName"]) in ("Role.DisplayName", "RoleDefinition.DisplayName")
| extend RoleAssignment = tostring(modifiedProperty["newValue"])
| where RoleAssignment contains "Admin"
| project
RoleAssignment_TimeGenerated = TimeGenerated,
RoleAssignment_OperationName = OperationName,
RoleAssignment_Result = Result,
RoleAssignment,
TargetType = tostring(TargetResources[0]["type"]),
Target = iff(isnotempty(TargetResources[0]["displayName"]), tostring(TargetResources[0]["displayName"]), tolower(TargetResources[0]["userPrincipalName"])),
TargetId = tostring(TargetResources[0]["id"]),
RoleAssignment_InitiatedBy = InitiatedBy,
RoleAssignment_TargetResources = TargetResources,
RoleAssignment_AdditionalDetails = AdditionalDetails,
RoleAssignment_CorrelationId = CorrelationId,
AppServicePrincipalId = tostring(InitiatedBy["app"]["servicePrincipalId"])
) on AppServicePrincipalId
Stage 13: where
where PermissionGrant_TimeGenerated < RoleAssignment_TimeGenerated
Stage 14: extend
extend TargetName, TargetUPNSuffix
Stage 15: project
project AppDisplayName, AppServicePrincipalId, PermissionGrant, PermissionGrant_AdditionalDetails, PermissionGrant_CorrelationId, PermissionGrant_InitiatedBy, PermissionGrant_OperationName, PermissionGrant_Result, PermissionGrant_TargetResources, PermissionGrant_TimeGenerated, RoleAssignment, RoleAssignment_AdditionalDetails, RoleAssignment_CorrelationId, RoleAssignment_InitiatedBy, RoleAssignment_OperationName, RoleAssignment_Result, RoleAssignment_TargetResources, RoleAssignment_TimeGenerated, Target, TargetId, TargetName, TargetType, TargetUPNSuffix
Stage 16: extend (8 consecutive steps)
extend PermissionGrant_InitiatingAadUserId, PermissionGrant_InitiatingAccountName, PermissionGrant_InitiatingAccountUPNSuffix, PermissionGrant_InitiatingIpAddress, PermissionGrant_InitiatingUserPrincipalName, RoleAssignment_InitiatingAadUserId, RoleAssignment_InitiatingAccountName, RoleAssignment_InitiatingAccountUPNSuffix, RoleAssignment_InitiatingIpAddress, RoleAssignment_InitiatingUserPrincipalName
Stage 17: summarize aggregation inside the mv-apply branch
summarize
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | eq |
| field:"AADOperationType" kind:eq value:"Assign" |
Category | eq |
| field:"Category" kind:eq |
InitiatedBy["app"] | is_not_null | ||
LoggedByService | eq |
| field:"azure_ad::logged_by_service" kind:eq |
OperationName | eq |
| field:"OperationName" kind:eq value:"Add app role assignment to service principal" |
PermissionGrant | match |
| field:"PermissionGrant" kind:match value:"RoleManagement.ReadWrite.Directory" |
RoleAssignment | contains |
| field:"RoleAssignment" kind:contains value:"Admin" |
modifiedProperty["displayName"] | eq |
| |
modifiedProperty["displayName"] | in |
|
CYFIRMA - Attack Surface - Domain/IP Vulnerability Exposure High Rule
#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 rule is triggered when CYFIRMA identifies publicly exposed vulnerabilities on domains or IP addresses within your organization's attack surface. These vulnerabilities may include outdated software, missing patches, insecure services, or misconfigurations that can be exploited by threat actors.Such exposure significantly increases the risk of exploitation, lateral movement, or data breach. Immediate investigation and remediation are recommended."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation | |
| Discovery | |
| Impact |
Rule Body
id: fbe4f5e0-d93e-4c93-8cf9-925eb8ea7f2e
name: CYFIRMA - Attack Surface - Domain/IP Vulnerability Exposure High Rule
description: |
"This rule is triggered when CYFIRMA identifies publicly exposed vulnerabilities on domains or IP addresses within your organization's attack surface. These vulnerabilities may include outdated software, missing patches, insecure services, or misconfigurations that can be exploited by threat actors.Such exposure significantly increases the risk of exploitation, lateral movement, or data breach. Immediate investigation and remediation are recommended."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaAttackSurfaceAlertsConnector
dataTypes:
- CyfirmaASDomainIPVulnerabilityAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Discovery
- DefenseEvasion
- Persistence
- Execution
- Impact
- PrivilegeEscalation
relevantTechniques:
- T1505
- T1068
- T1046
- T1499
query: |
// High Severity Domain/IP Vulnerability Exposure Detected
let timeFrame = 5m;
CyfirmaASDomainIPVulnerabilityAlerts_CL
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
| mv-expand pvuln = possible_vulnerabilities
| extend cve = tostring(pvuln.cveNo), vulProducts = pvuln.products
| mv-expand vul_Products = vulProducts
| summarize cveList = make_set(cve, 100), vul_products1 = make_set(vul_Products, 100) by uid
| join kind=inner (CyfirmaASDomainIPVulnerabilityAlerts_CL
| where severity == 'Critical'
| summarize arg_max(TimeGenerated, *) by uid)
on uid
| extend
Vulnerabilities = strcat_array(cveList, ', '),
VulnerabilityProducts = strcat_array(vul_products1, ', '),
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=sub_domain,
TopDomain=top_domain,
NetworkIP=ip,
AlertUID=alert_uid,
UID=uid,
OpenPorts=open_ports,
HostProvider=host_provider,
Country=country,
Softwares=softwares,
WebServer=web_server,
WebServerVersion=web_server_version,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
Domain,
TopDomain,
RiskScore,
FirstSeen,
LastSeen,
NetworkIP,
AlertUID,
UID,
Vulnerabilities,
VulnerabilityProducts,
OpenPorts,
HostProvider,
Country,
Softwares,
WebServer,
WebServerVersion,
ProviderName,
ProductName
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: TopDomain
- identifier: DnsDomain
columnName: Domain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: NetworkIP
customDetails:
TimeGenerated: TimeGenerated
RiskScore: RiskScore
FirstSeen: FirstSeen
LastSeen: LastSeen
Vulnerabilities: Vulnerabilities
UID: UID
OpenPorts: OpenPorts
WebServer: WebServer
WebServerVersion: WebServerVersion
Softwares: Softwares
HostProvider: HostProvider
AlertUID: AlertUID
Country: Country
vulnerableProducts: VulnerabilityProducts
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - High Severity Domain/IP Vulnerability Exposure Detected - Domain: {{Domain}}, IP: {{NetworkIP}}"
alertDescriptionFormat: "CYFIRMA - High Severity Domain/IP Vulnerability Exposure Detected - {{Description}}"
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaASDomainIPVulnerabilityAlerts_CL
Stage 2: where
| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: mv-expand
| mv-expand pvuln = possible_vulnerabilities
Stage 4: extend
| extend cve = tostring(pvuln.cveNo), vulProducts = pvuln.products
Stage 5: mv-expand
| mv-expand vul_Products = vulProducts
Stage 6: summarize
| summarize cveList = make_set(cve, 100), vul_products1 = make_set(vul_Products, 100) by uid
Stage 7: join
| join kind=inner (CyfirmaASDomainIPVulnerabilityAlerts_CL
| where severity == 'Critical'
| summarize arg_max(TimeGenerated, *) by uid)
on uid
Stage 8: extend
extend AlertUID, Country, Description, Domain, FirstSeen, HostProvider, LastSeen, NetworkIP, OpenPorts, ProductName, ProviderName, RiskScore, Softwares, TopDomain, UID, Vulnerabilities, VulnerabilityProducts, WebServer, WebServerVersion
Stage 9: project
project AlertUID, Country, Description, Domain, FirstSeen, HostProvider, LastSeen, NetworkIP, OpenPorts, ProductName, ProviderName, RiskScore, Softwares, TimeGenerated, TopDomain, UID, Vulnerabilities, VulnerabilityProducts, WebServer, WebServerVersion
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"Critical" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
Country | project |
Description | project |
Domain | project |
FirstSeen | project |
HostProvider | project |
LastSeen | project |
NetworkIP | project |
OpenPorts | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
Softwares | project |
TimeGenerated | project |
TopDomain | project |
UID | project |
Vulnerabilities | project |
VulnerabilityProducts | project |
WebServer | project |
WebServerVersion | project |
CYFIRMA - Compromised Employees Detection Rule
#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.
"Identifies and alerts on internal employee accounts that have been compromised, based on CYFIRMA's threat intelligence. This rule captures the latest exposure of user credentials, IP addresses, hostnames, operating systems, and pass hashes observed in the threat feed. It supports rapid detection and investigation of phishing, stealer malware, and insider compromise scenarios."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Credential Access |
Rule Body
id: 72d3fb86-d1eb-44d6-9352-170c6bb45bb7
name: CYFIRMA - Compromised Employees Detection Rule
description: |
"Identifies and alerts on internal employee accounts that have been compromised, based on CYFIRMA's threat intelligence.
This rule captures the latest exposure of user credentials, IP addresses, hostnames, operating systems, and pass hashes observed in the threat feed.
It supports rapid detection and investigation of phishing, stealer malware, and insider compromise scenarios."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
- connectorId: CyfirmaCompromisedAccountsDataConnector
dataTypes:
- CyfirmaCompromisedAccounts_CL
queryFrequency: 5m
queryPeriod: 5m
suppressionDuration: 6h
suppressionEnabled: true
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
- InitialAccess
- Persistence
relevantTechniques:
- T1003
- T1552
- T1078
- T1098
query: |
// Compromised Employees - Latest per UID
let timeFrame = 5m;
CyfirmaCompromisedAccounts_CL
| where TimeGenerated between (ago(timeFrame) .. now())
and Category has "Compromised Employees"
| extend
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
| summarize arg_max(TimeGenerated,
url,
ip,
email,
user_name,
computer_name,
operating_system,
breach_date,
first_seen,
last_seen,
impact,
recommendations,
description,
source,
pass_hash,
ProductName,
ProviderName
) by uid
| sort by TimeGenerated desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: user_name
- identifier: UPNSuffix
columnName: email
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: computer_name
- identifier: OSVersion
columnName: operating_system
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ip
- entityType: URL
fieldMappings:
- identifier: Url
columnName: url
customDetails:
UID: uid
TimeGenerated: TimeGenerated
BreachDate: breach_date
FirstSeen: first_seen
LastSeen: last_seen
Impact: impact
Recommendations: recommendations
Source: source
Description: description
PassHash: pass_hash
alertDetailsOverride:
alertDisplayNameFormat: "Employee Compromised - {{user_name}} - {{email}}"
alertDescriptionFormat: "{{description}}"
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaCompromisedAccounts_CL
Stage 2: where
| where TimeGenerated between (ago(timeFrame) .. now())
and Category has "Compromised Employees"
Stage 3: extend
| extend
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
Stage 4: summarize
| summarize arg_max(TimeGenerated,
url,
ip,
email,
user_name,
computer_name,
operating_system,
breach_date,
first_seen,
last_seen,
impact,
recommendations,
description,
source,
pass_hash,
ProductName,
ProviderName
) by uid
Stage 5: sort
| sort by TimeGenerated desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | match |
| field:"Category" kind:match value:"Compromised Employees" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
uid | summarize |
Detect entra token request via specific BOF (IOC based)
#This might be one of the silliest detections I have created. But since there is a Beacon Object File out there which can be used to directly request Entra ID access tokens from an active beacon on a device using a specific User Agent, we can easily detect this beacon file by flagging the funny user agent and / or scope identifier that is used.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Credential Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Target application: Sign-in naming a target application |
Rule Body
AADNonInteractiveUserSignInLogs
| where TimeGenerated > ago(5m)
| where UserAgent contains "ur mum"
| where ResourceIdentity == "797f4846-ba00-4fd7-ba43-dac1f8f63013"
Stages and Predicates
Stage 1: source
AADNonInteractiveUserSignInLogs
Stage 2: where
| where TimeGenerated > ago(5m)
Stage 3: where
| where UserAgent contains "ur mum"
Stage 4: where
| where ResourceIdentity == "797f4846-ba00-4fd7-ba43-dac1f8f63013"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ResourceIdentity | eq |
| field:"ResourceIdentity" kind:eq value:"797f4846-ba00-4fd7-ba43-dac1f8f63013" |
UserAgent | contains |
| field:"aws::userAgent" kind:contains value:"ur mum" |
Detect Local File Inclusion(LFI) in web requests (ASIM Web Session)
#'LFI vulnerabilities allow an attacker to read (and sometimes execute) files on the victim machine. This can be very dangerous because if the web server is misconfigured and running with high privileges, the attacker may gain access to sensitive information'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution |
Rule Body
id: 7bb55d05-ef39-4a40-8079-0bc3c05e7881
name: Detect Local File Inclusion(LFI) in web requests (ASIM Web Session)
description: |
'LFI vulnerabilities allow an attacker to read (and sometimes execute) files on the victim machine. This can be very dangerous because if the web server is misconfigured and running with high privileges, the attacker may gain access to sensitive information'
severity: High
status: Available
tags:
- Schema: WebSession
SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Execution
relevantTechniques:
- T1190
- T1133
- T1059
query: |
let lookback = 5m;
let LFI_Indicators = materialize(externaldata(Indicators: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/LocalFileInclusionIndicators.csv"]
with(format="csv", ignoreFirstRecord=True));
let CustomLocalFileInclusionIndicators = (_ASIM_GetWatchlistRaw("Web_LocalFileInclusionIndicators") // Create new Watchlist and add your custom indicators(Optional)
| extend
Indicators = tostring(WatchlistItem["Indicators"])
| project Indicators
| where isnotempty(Indicators));
let CombinedLFIList = union LFI_Indicators, CustomLocalFileInclusionIndicators;
let knownLFIIndicators=toscalar(CombinedLFIList
| where isnotempty(Indicators)
| summarize make_set(Indicators, 1000));
_Im_WebSession(starttime=ago(lookback), url_has_any=knownLFIIndicators, eventresult='Success')
| where isnotempty(Url)
| project Url, SrcIpAddr, SrcUsername, SrcHostname, TimeGenerated
| extend Decoded_url = url_decode(Url)
| where Decoded_url has_any (knownLFIIndicators)
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url
| extend
Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
| order by EventCount desc
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SrcHostname
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
EventCount: EventCount
Decoded_url: Decoded_url
EventStartTime: EventStartTime
EventEndTime: EventEndTime
alertDetailsOverride:
alertDisplayNameFormat: "Potential Local File Inlcusion(LFI) performed by user '{{SrcUsername}}' from IP '{{SrcIpAddr}}'"
alertDescriptionFormat: "User requested for URL '{{Url}}' which contains LFI related keywords or indicators. It suggests an attempt to traverse directories and access files outside the intended directory structure"
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 5m;
let CombinedLFIList = union LFI_Indicators, CustomLocalFileInclusionIndicators;
Let Binding: LFI_Indicators
let LFI_Indicators = materialize(externaldata(Indicators: string)
[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/LocalFileInclusionIndicators.csv"]
with(format="csv", ignoreFirstRecord=True));
Let Binding: CustomLocalFileInclusionIndicators
let CustomLocalFileInclusionIndicators = (_ASIM_GetWatchlistRaw("Web_LocalFileInclusionIndicators")
| extend
Indicators = tostring(WatchlistItem["Indicators"])
| project Indicators
| where isnotempty(Indicators));
Let Binding: knownLFIIndicators
let knownLFIIndicators = toscalar(CombinedLFIList
| where isnotempty(Indicators)
| summarize make_set(Indicators, 1000));
Stage 1: source
_Im_WebSession(starttime=ago(lookback), url_has_any=knownLFIIndicators, eventresult='Success')
Stage 2: where
| where isnotempty(Url)
Stage 3: project
| project Url, SrcIpAddr, SrcUsername, SrcHostname, TimeGenerated
Stage 4: extend
| extend Decoded_url = url_decode(Url)
Stage 5: where
| where Decoded_url has_any (knownLFIIndicators)
Stage 6: summarize
| summarize
EventCount=count(),
EventStartTime=min(TimeGenerated),
EventEndTime=max(TimeGenerated)
by SrcIpAddr, SrcUsername, SrcHostname, Url, Decoded_url
Stage 7: extend
| extend
Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
Stage 8: sort
| order by EventCount desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Decoded_url | match |
| field:"Decoded_url" kind:match value:"knownLFIIndicators" |
Url | is_not_null | field:"Url" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Decoded_url | summarize |
EventCount | summarize |
EventEndTime | summarize |
EventStartTime | summarize |
SrcHostname | summarize |
SrcIpAddr | summarize |
SrcUsername | summarize |
Url | summarize |
Name | extend |
UPNSuffix | extend |
Detect LolDriver drop or load from unknown or unsigned process
#Adversaries may use LolDrivers to elevate their privileges on a system. Regularly, their drop their own LolDrivers from their beacon process when the LolDriver is not yet present on the system. This is a detection use case to detect an unknown process dropping these LolDrivers.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
References
Telemetry Coverage
Rule Body
let LOLDrivers = externaldata(Category:string, KnownVulnerableSamples:dynamic, Verified:string ) [h@"https://www.loldrivers.io/api/drivers.json"]
with (
format=multijson,
ingestionMapping=@'[{"Column":"Category","Properties":{"Path":"$.Category"}},{"Column":"KnownVulnerableSamples","Properties":{"Path":"$.KnownVulnerableSamples"}},{"Column":"Verified","Properties":{"Path":"$.Verified"}}]'
)
| mv-expand KnownVulnerableSamples
| extend SHA1 = tostring(KnownVulnerableSamples.SHA1), SHA256 = tostring(KnownVulnerableSamples.SHA256)
;
let SHA1List = toscalar(
LOLDrivers
| summarize make_set(SHA1)
);
let SHA256List = toscalar(
LOLDrivers
| summarize make_set(SHA256)
);
let device_events = (
DeviceEvents
| where Timestamp > ago(1h)
| where ActionType == "DriverLoaded"
| where SHA1 in ( SHA1List ) or SHA256 in ( SHA256List )
);
let device_file_events = (
DeviceFileEvents
| where Timestamp > ago(1h)
| where ActionType == "FileCreated"
| where SHA1 in ( SHA1List ) or SHA256 in ( SHA256List )
);
union device_events, device_file_events
| invoke FileProfile(InitiatingProcessSHA1)
| where GlobalPrevalence < 1000 or SignatureState =~ "Unsigned"
Stages and Predicates
Let Binding: LOLDrivers
let LOLDrivers = externaldata(Category:string, KnownVulnerableSamples:dynamic, Verified:string ) [h@"https://www.loldrivers.io/api/drivers.json"]
with (
format=multijson,
ingestionMapping=@'[{"Column":"Category","Properties":{"Path":"$.Category"}},{"Column":"KnownVulnerableSamples","Properties":{"Path":"$.KnownVulnerableSamples"}},{"Column":"Verified","Properties":{"Path":"$.Verified"}}]'
)
| mv-expand KnownVulnerableSamples
| extend SHA1 = tostring(KnownVulnerableSamples.SHA1), SHA256 = tostring(KnownVulnerableSamples.SHA256);
Let Binding: SHA1List
let SHA1List = toscalar(
LOLDrivers
| summarize make_set(SHA1)
);
Let Binding: SHA256List
let SHA256List = toscalar(
LOLDrivers
| summarize make_set(SHA256)
);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: device_events, device_file_events
Leg 1: device_events
Leg 2: device_file_events
Applied to the Combined Result
| invoke FileProfile(InitiatingProcessSHA1) | where GlobalPrevalence < 1000 or SignatureState =~ "Unsigned"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"1000" |
SHA1 | in |
| field:"SHA1" kind:in value:"SHA1List" |
SHA256 | in |
| field:"SHA256" kind:in value:"SHA256List" |
SignatureState | eq |
| field:"SignatureState" kind:eq value:"Unsigned" |
DLL Hijacking: Loading from an Unusual Directory
#Below query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different folder and making an application load it instead of the original DLL.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Stealth |
Telemetry Coverage
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post: https://posts.bluraven.io/detecting-dll-hijacking-attacks-part-1-bdb354685164
// Description: This query detects DLL Hijacking scenario of planting a DLL having an invalid signature in a different
// folder and making an application load it instead of the original DLL.
//
// Query parameters:
let WinDevices = materialize (
DeviceInfo
| where Timestamp > ago(8d)
| where OSPlatform startswith_cs "Windows"
| summarize make_set(DeviceId)
)
;
// Get Filenames that have more than 1 SHA1 and loaded by the same process
let FileNames = materialize (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| where isnotempty(SHA1) and isnotempty(InitiatingProcessFileName)
| project FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| summarize hint.strategy=shuffle dcount(SHA1) by FileName, Process
| where dcount_SHA1 > 1
)
;
// From the Filenames, get SHA1 values and filter Filename-SHA1 if its loaded by a few proecsses and on a few devices
// Also, get first and last time of load of the file(based on SHA1, filename)
let Files = materialize (
FileNames
| join hint.strategy=shuffle kind=rightsemi (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| project Timestamp, DeviceName, FileName = tolower(FileName), SHA1, Process = tolower(InitiatingProcessFileName), DLLDir = tolower(tostring(parse_path(FolderPath).DirectoryPath)), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or (not(DLLDir startswith "c:\\windows"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| summarize hint.strategy=shuffle dcount(DeviceName), Process = make_set(Process), FirstLoad = min(Timestamp), LastLoad = max(Timestamp), count() by FileName, SHA1
| where dcount_DeviceName < 3 and array_length(Process) < 3
| mv-expand Process to typeof(string)
)
on FileName, Process
)
;
// Files: Potentially suspicious files seen in the last 8d
// Next step: suspicious file (based on SHA1) should have been loaded from a location different than the other files(SHA1s) based on the same filename
// The suspicious file should have also been loaded recently
// Get files loaded in the last day (from the potentially suspicious files)
let Hashes = materialize (
Files
| where FirstLoad > ago(1d)
| join (
DeviceFileCertificateInfo
| where Timestamp > ago(30d)
| where not(IsTrusted )
| summarize arg_max(Timestamp,*) by SHA1
| project-away Timestamp, DeviceId, DeviceName) on SHA1
| project-away SHA11
)
;
// Get all image loads of the files that have the same name with the files in Hashes table(Hashes table only has the suspicious hash with its name from the last day)
Hashes
| join hint.strategy=shuffle kind=inner (
DeviceImageLoadEvents
| where Timestamp > ago(8d)
| where DeviceId in (WinDevices)
| project Timestamp, DeviceName, SHA1, FolderPath, FileName = tolower(FileName), Process = tolower(InitiatingProcessFileName), DLLDirectory = strcat(tolower(tostring(parse_path(FolderPath).DirectoryPath)), '\\'), ProcessDir = tolower(tostring(parse_path(InitiatingProcessFolderPath).DirectoryPath))
| where DLLDirectory in ("c:\\windows\\", "c:\\windows\\system32\\", "c:\\windows\\syswow64\\", "c:\\windows\\winsxs\\") or (not(DLLDirectory startswith "c:\\windows\\"))
| where ProcessDir in ("c:\\windows", "c:\\windows\\system32", "c:\\windows\\syswow64", "c:\\windows\\winsxs") or ProcessDir has_all ("Users","AppData") or ProcessDir has_any ("Program Files")
| extend NormalizedDLLDirectory = replace(@'(c|d):\\users\\.*?\\', @'c:\\users\\userxx\\',DLLDirectory)
| extend NormalizedDLLDirectory = replace(@'\{.*\}', @'\{xxxxxxxxxx\}',NormalizedDLLDirectory) //{fe07d7-d438-4dd9-bb0f-5721658f4f}
| extend NormalizedDLLDirectory = replace(@'\\[A-Za-z0-9-]+-[A-Za-z0-9]+\\', @'\\xxxxxxxxxx\\',NormalizedDLLDirectory ) //\fe07d7-d438-4dd9-bb0f-5721658f4f\
| extend NormalizedDLLDirectory = replace(@'\d+\.\d+\.\d+\.\d+', @'X.Y.Z.T',NormalizedDLLDirectory) // ex: Edge\Application\104.0.1293.47\process.exe
| extend NormalizedDLLDirectory = replace(@'-\d+\.\d+\.\d+', @'-X.Y.Z',NormalizedDLLDirectory)
| extend NormalizedDLLDirectory = replace(@'c:\\windows\\assembly\\nativeimages_v\d\.\d\.\d+_\d{2}\\.*', @'c:\\windows\\assembly\\nativeimages_vX.Y.Z_T\\oneoffewsubfolders\\', NormalizedDLLDirectory)
| extend NormalizedDLLDirectory = replace(@'c:\\programdata\\.*?\\microsoft\\teams\\',@'c:\\programdata\\userxxx\\microsoft\\teams\\',NormalizedDLLDirectory)
| summarize hint.strategy=shuffle by FileName, SHA1, NormalizedDLLDirectory
)
on FileName
| project-rename OtherSHA1 = SHA11
// Flag suspicious hash
| extend Suspicious = iff(SHA1==OtherSHA1, 'TRUE', 'FALSE')
// group properties of suspicious and previous files separately
// we are looking for a filename that was loaded from previously unknown location
| summarize hint.strategy=shuffle PreviousDirs = make_set_if(NormalizedDLLDirectory, Suspicious == 'FALSE'),
NewDir = make_set_if(NormalizedDLLDirectory, Suspicious == 'TRUE'),
PreviousSHA1s = make_set_if(OtherSHA1, Suspicious == 'FALSE'),
NewSHA1 = make_set_if(SHA1, Suspicious == 'TRUE')
by FileName
// compare the directory of the suspicous file with the previous directories
| extend diff = set_difference(NewDir, PreviousDirs)
// filter if the new(suspicious) file is loaded from previously known directory
| where diff != '[]'
| order by FileName
// if you get lots of false positives, uncomment the below section.
// this section compares the directory names in an alternative way.
// | mv-expand NewDir to typeof(string), PreviousDirs to typeof(string)
// | extend prev = split(PreviousDirs, '\\'), new = split(NewDir, '\\')
// | extend diff_new = set_difference(new, prev)
// | extend diff_count = array_length(diff_new)
// | where diff_count > 1
// | project-away prev, new, diff_count, diff
// get file profile info and filter based on global prevalence
| mv-expand NewSHA1 to typeof(string)
| invoke FileProfile(NewSHA1, 1000)
| where GlobalPrevalence < 200 or isempty(GlobalPrevalence)
Stages and Predicates
let FileNames, let Files and let Hashes are inlined into the numbered stages below.
Let Binding: WinDevices
let WinDevices = materialize (
DeviceInfo
| where Timestamp > ago(8d)
| where OSPlatform startswith_cs "Windows"
| summarize make_set(DeviceId)
);
Stage 1: source
let WinDevices
Stage 2: source
let FileNames
Stage 3: source
let Files
Stage 4: source
let Hashes
Stage 5: source
DeviceImageLoadEvents
Stage 6: where
where Timestamp > ago(691200s)
Stage 7: where
where DeviceId == "WinDevices"
Stage 8: where
where isnotempty(InitiatingProcessFileName) and isnotempty(SHA1)
Stage 9: project
project DLLDir, FileName, Process, ProcessDir, SHA1
Stage 10: where
where (not (DLLDir startswith @"c:\windows") or DLLDir in (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs"))
Stage 11: where
where ((ProcessDir has "AppData" and ProcessDir has "Users") or ProcessDir in (@"c:\windows", @"c:\windows\system32", @"c:\windows\syswow64", @"c:\windows\winsxs") or ProcessDir has "Program Files")
Stage 12: summarize
summarize by FileName, Process
Stage 13: where
where dcount_SHA1 > 1
Stage 14: join
join kind=rightsemi (DeviceImageLoadEvents) on FileName, Process
Stage 15: where
where FirstLoad > ago(86400s)
Stage 16: join
join (DeviceFileCertificateInfo) on SHA1
Stage 17: project-away
project-away SHA11
Stage 18: join
join kind=inner (DeviceImageLoadEvents) on FileName
Stage 19: project-rename
project-rename
Stage 20: extend
extend Suspicious
Suspicious =SHA1 =~ OtherSHA1'TRUE''FALSE'Stage 21: summarize
summarize NewDir, NewSHA1, PreviousDirs, PreviousSHA1s by FileName
Stage 22: extend
extend diff
Stage 23: where
where diff != "[]"
Stage 24: sort
sort by FileName
Stage 25: mv-expand
mv-expand NewSHA1
Stage 26: invoke
invoke
Stage 27: where
where (isempty(GlobalPrevalence) or GlobalPrevalence < 200)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DLLDir | in |
| field:"DLLDir" kind:in |
DLLDirectory | in |
| field:"DLLDirectory" kind:in |
DeviceId | in |
| field:"DeviceId" kind:in value:"WinDevices" |
GlobalPrevalence | is_null | field:"GlobalPrevalence" kind:is_null | |
GlobalPrevalence | lt |
| field:"GlobalPrevalence" kind:lt value:"200" |
InitiatingProcessFileName | is_not_null | field:"InitiatingProcessFileName" kind:is_not_null | |
ProcessDir | in |
| field:"ProcessDir" kind:in |
ProcessDir | match |
| field:"ProcessDir" kind:match |
SHA1 | is_not_null | field:"SHA1" kind:is_not_null | |
dcount_DeviceName | lt |
| field:"dcount_DeviceName" kind:lt value:"3" |
dcount_SHA1 | gt |
| field:"dcount_SHA1" kind:gt value:"1" |
diff | ne |
| field:"diff" kind:ne value:"[]" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
IsTrusted | eq | true | excludes:IsTrusted field:"IsTrusted" value:"true" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
FileName | summarize |
NewDir | summarize |
NewSHA1 | summarize |
PreviousDirs | summarize |
PreviousSHA1s | summarize |
diff | extend |
Fake computer account created
#This query detects domain user accounts creation (event ID 4720) where the username ends with $. Accounts that end with $ are normally domain computer accounts and when they are created the event ID 4741 is generated instead.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4720: A user account was created. |
Rule Body
id: c1faf5e8-6958-11ec-90d6-0242ac120003
name: Fake computer account created
description: |
'This query detects domain user accounts creation (event ID 4720) where the username ends with $.
Accounts that end with $ are normally domain computer accounts and when they are created the event ID 4741 is generated instead.'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1564
query: |
SecurityEvent
| where EventID == 4720 and TargetUserName endswith "$"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectUserName, SubjectDomainName, SubjectAccount, SubjectUserSid, SubjectLogonId,
TargetUserName, TargetDomainName, TargetAccount, TargetSid, UserPrincipalName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: SubjectAccount
- identifier: Name
columnName: SubjectUserName
- identifier: NTDomain
columnName: SubjectDomainName
- entityType: Account
fieldMappings:
- identifier: Sid
columnName: SubjectUserSid
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: TargetAccount
- identifier: Name
columnName: TargetUserName
- identifier: NTDomain
columnName: TargetDomainName
- entityType: Account
fieldMappings:
- identifier: Sid
columnName: TargetSid
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: HostNameDomain
version: 1.0.4
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Vasileios Paschalidis
support:
tier: Community
categories:
domains: [ "Security - Others" ]
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 4720 and TargetUserName endswith "$"
Stage 3: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by Computer, SubjectUserName, SubjectDomainName, SubjectAccount, SubjectUserSid, SubjectLogonId,
TargetUserName, TargetDomainName, TargetAccount, TargetSid, UserPrincipalName
Stage 4: extend
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
Stage 5: extend
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =DomainIndex != -1substring(Computer, (DomainIndex + 1))ComputerStage 6: project-away
| project-away DomainIndex
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4720" |
TargetUserName | ends_with |
| field:"TargetUserName" kind:ends_with value:"$" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
EndTime | summarize |
StartTime | summarize |
SubjectAccount | summarize |
SubjectDomainName | summarize |
SubjectLogonId | summarize |
SubjectUserName | summarize |
SubjectUserSid | summarize |
TargetAccount | summarize |
TargetDomainName | summarize |
TargetSid | summarize |
TargetUserName | summarize |
UserPrincipalName | summarize |
HostName | extend |
HostNameDomain | extend |
Lookout - Critical Smishing and Phishing Alerts (v2)
#'Detects critical smishing (SMS phishing) and phishing alerts from Lookout Mobile Risk API v2. This rule identifies sophisticated social engineering attacks including CEO fraud, credential harvesting, and malicious link campaigns targeting mobile devices. Leverages enhanced v2 smishing detection capabilities for comprehensive mobile threat protection.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access | |
| Discovery | |
| Collection |
Rule Body
id: 7a3e5f9b-4c8d-4a2e-9f1b-6d8e2a4c7f9e
name: Lookout - Critical Smishing and Phishing Alerts (v2)
description: |
'Detects critical smishing (SMS phishing) and phishing alerts from Lookout Mobile Risk API v2. This rule identifies sophisticated social engineering attacks including CEO fraud, credential harvesting, and malicious link campaigns targeting mobile devices. Leverages enhanced v2 smishing detection capabilities for comprehensive mobile threat protection.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: LookoutAPI
dataTypes:
- LookoutEvents
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
- Collection
- Discovery
relevantTechniques:
- T1660
- T1417
- T1423
query: |
LookoutEvents
| where EventType == "SMISHING_ALERT"
| where SmishingAlertSeverity in ("CRITICAL", "HIGH")
| where SmishingAlertType in ("PHISHING_DETECTION", "FRAUD_DETECTION", "CREDENTIAL_HARVESTING")
| extend
AlertRiskScore = case(
SmishingAlertSeverity == "CRITICAL", 10,
SmishingAlertSeverity == "HIGH", 8,
SmishingAlertSeverity == "MEDIUM", 5,
SmishingAlertSeverity == "LOW", 2,
1
),
ThreatCategory = case(
SmishingAlertType == "PHISHING_DETECTION", "Phishing",
SmishingAlertType == "FRAUD_DETECTION", "Fraud",
SmishingAlertType == "CREDENTIAL_HARVESTING", "Credential Theft",
SmishingAlertType == "MALICIOUS_LINK", "Malicious Link",
"Other"
),
ImpersonationRisk = case(
SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive", "Executive Impersonation",
SmishingAlertDescription has "IT" or SmishingAlertDescription has "support", "IT Support Impersonation",
SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial", "Financial Impersonation",
SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package", "Delivery Impersonation",
"Generic Phishing"
)
| extend DeviceRiskLevel = case(
DeviceSecurityStatus == "THREATS_HIGH", "High",
DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
DeviceSecurityStatus == "THREATS_LOW", "Low",
"Unknown"
)
| extend CampaignIndicators = case(
AlertRiskScore >= 8 and DeviceRiskLevel == "High", "Targeted Campaign",
AlertRiskScore >= 6 and ImpersonationRisk != "Generic Phishing", "Sophisticated Attack",
AlertRiskScore >= 5, "Coordinated Threat",
"Isolated Incident"
)
| project
TimeGenerated,
EventId,
SmishingAlertId,
SmishingAlertType,
SmishingAlertSeverity,
SmishingAlertDescription,
AlertRiskScore,
ThreatCategory,
ImpersonationRisk,
CampaignIndicators,
DeviceGuid,
DevicePlatform,
DeviceOSVersion,
DeviceManufacturer,
DeviceModel,
DeviceEmailAddress,
DeviceSecurityStatus,
DeviceRiskLevel,
TargetEmailAddress,
TargetPlatform,
ActorType,
ActorGuid,
ChangeType
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: DeviceEmailAddress
- identifier: Name
columnName: TargetEmailAddress
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceGuid
- identifier: OSFamily
columnName: DevicePlatform
- identifier: OSVersion
columnName: DeviceOSVersion
- entityType: URL
fieldMappings:
- identifier: Url
columnName: SmishingAlertDescription
customDetails:
SmishAlertType: SmishingAlertType
SmishSeverity: SmishingAlertSeverity
AlertRiskScore: AlertRiskScore
ThreatCategory: ThreatCategory
ImpersonationRisk: ImpersonationRisk
CampaignIndicators: CampaignIndicators
DevicePlatform: DevicePlatform
DeviceSecStatus: DeviceSecurityStatus
DeviceRiskLevel: DeviceRiskLevel
alertDetailsOverride:
alertDisplayNameFormat: "Critical Smishing Alert: {{ThreatCategory}} targeting {{DevicePlatform}} Device"
alertDescriptionFormat: "{{SmishingAlertSeverity}} {{ThreatCategory}} attack on {{DevicePlatform}}"
alertTacticsColumnName: ThreatCategory
alertSeverityColumnName: SmishingAlertSeverity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: Selected
groupByEntities:
- Account
- Host
groupByAlertDetails:
- SmishAlertType
- DeviceGuid
groupByCustomDetails:
- ThreatCategory
- ImpersonationRisk
- CampaignIndicators
eventGroupingSettings:
aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 2.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
LookoutEvents
Stage 2: where
| where EventType == "SMISHING_ALERT"
Stage 3: where
| where SmishingAlertSeverity in ("CRITICAL", "HIGH")
Stage 4: where
| where SmishingAlertType in ("PHISHING_DETECTION", "FRAUD_DETECTION", "CREDENTIAL_HARVESTING")
Stage 5: extend (3 consecutive steps)
| extend
AlertRiskScore = case(
SmishingAlertSeverity == "CRITICAL", 10,
SmishingAlertSeverity == "HIGH", 8,
SmishingAlertSeverity == "MEDIUM", 5,
SmishingAlertSeverity == "LOW", 2,
1
),
ThreatCategory = case(
SmishingAlertType == "PHISHING_DETECTION", "Phishing",
SmishingAlertType == "FRAUD_DETECTION", "Fraud",
SmishingAlertType == "CREDENTIAL_HARVESTING", "Credential Theft",
SmishingAlertType == "MALICIOUS_LINK", "Malicious Link",
"Other"
),
ImpersonationRisk = case(
SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive", "Executive Impersonation",
SmishingAlertDescription has "IT" or SmishingAlertDescription has "support", "IT Support Impersonation",
SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial", "Financial Impersonation",
SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package", "Delivery Impersonation",
"Generic Phishing"
)
| extend DeviceRiskLevel = case(
DeviceSecurityStatus == "THREATS_HIGH", "High",
DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
DeviceSecurityStatus == "THREATS_LOW", "Low",
"Unknown"
)
| extend CampaignIndicators = case(
AlertRiskScore >= 8 and DeviceRiskLevel == "High", "Targeted Campaign",
AlertRiskScore >= 6 and ImpersonationRisk != "Generic Phishing", "Sophisticated Attack",
AlertRiskScore >= 5, "Coordinated Threat",
"Isolated Incident"
)
AlertRiskScore =SmishingAlertSeverity == "CRITICAL"10SmishingAlertSeverity == "HIGH"8SmishingAlertSeverity == "MEDIUM"5SmishingAlertSeverity == "LOW"21ImpersonationRisk =(SmishingAlertDescription has "CEO" or SmishingAlertDescription has "executive")"Executive Impersonation"(SmishingAlertDescription has "IT" or SmishingAlertDescription has "support")"IT Support Impersonation"(SmishingAlertDescription has "bank" or SmishingAlertDescription has "financial")"Financial Impersonation"(SmishingAlertDescription has "delivery" or SmishingAlertDescription has "package")"Delivery Impersonation""Generic Phishing"ThreatCategory =SmishingAlertType == "PHISHING_DETECTION""Phishing"SmishingAlertType == "FRAUD_DETECTION""Fraud"SmishingAlertType == "CREDENTIAL_HARVESTING""Credential Theft"SmishingAlertType == "MALICIOUS_LINK""Malicious Link""Other"Stage 6: project
| project
TimeGenerated,
EventId,
SmishingAlertId,
SmishingAlertType,
SmishingAlertSeverity,
SmishingAlertDescription,
AlertRiskScore,
ThreatCategory,
ImpersonationRisk,
CampaignIndicators,
DeviceGuid,
DevicePlatform,
DeviceOSVersion,
DeviceManufacturer,
DeviceModel,
DeviceEmailAddress,
DeviceSecurityStatus,
DeviceRiskLevel,
TargetEmailAddress,
TargetPlatform,
ActorType,
ActorGuid,
ChangeType
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"SMISHING_ALERT" |
SmishingAlertSeverity | in |
| field:"SmishingAlertSeverity" kind:in |
SmishingAlertType | in |
| field:"SmishingAlertType" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorGuid | project |
ActorType | project |
AlertRiskScore | project |
CampaignIndicators | project |
ChangeType | project |
DeviceEmailAddress | project |
DeviceGuid | project |
DeviceManufacturer | project |
DeviceModel | project |
DeviceOSVersion | project |
DevicePlatform | project |
DeviceRiskLevel | project |
DeviceSecurityStatus | project |
EventId | project |
ImpersonationRisk | project |
SmishingAlertDescription | project |
SmishingAlertId | project |
SmishingAlertSeverity | project |
SmishingAlertType | project |
TargetEmailAddress | project |
TargetPlatform | project |
ThreatCategory | project |
TimeGenerated | project |
Non Domain Controller Active Directory Replication
#This query detects potential attempts by non-computer accounts (non domain controllers) to retrieve/synchronize an active directory object leveraging directory replication services (DRS). A Domain Controller (computer account) would usually be performing these actions in a domain environment. Another detection rule can be created to cover domain controllers accounts doing at rare times. A domain user with privileged permissions to use directory replication services is rare.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Telemetry Coverage
Rule Body
id: b9d2eebc-5dcb-4888-8165-900db44443ab
name: Non Domain Controller Active Directory Replication
description: |
'This query detects potential attempts by non-computer accounts (non domain controllers) to retrieve/synchronize an active directory object leveraging directory replication services (DRS).
A Domain Controller (computer account) would usually be performing these actions in a domain environment. Another detection rule can be created to cover domain controllers accounts doing at rare times.
A domain user with privileged permissions to use directory replication services is rare.'
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1003.006
query: |
// Enter a reference list of hostnames for your DC servers
//let DCServersList = dynamic (["DC01.simulandlabs.com","DC02.simulandlabs.com"]);
SecurityEvent
//| where Computer in (DCServersList)
| where EventID == 4662 and ObjectServer == 'DS'
| where AccountType != 'Machine'
| where Properties has '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2' //DS-Replication-Get-Changes
or Properties has '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2' //DS-Replication-Get-Changes-All
or Properties has '89e95b76-444d-4c62-991a-0facbeda640c' //DS-Replication-Get-Changes-In-Filtered-Set
| project TimeGenerated, Account, Activity, Properties, SubjectLogonId, Computer
| join kind=leftouter
(
SecurityEvent
//| where Computer in (DCServersList)
| where EventID == 4624 and LogonType == 3
| where AccountType != 'Machine'
| project TargetLogonId, IpAddress, Computer
)
on $left.SubjectLogonId == $right.TargetLogonId and $left.Computer == $right.Computer
| project-reorder TimeGenerated, Computer, Account, IpAddress
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountNTDomain = tostring(split(Account, "\\")[0]), AccountName = tostring(split(Account, "\\")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountNTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.6
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 4662 and ObjectServer == 'DS'
Stage 3: where
| where AccountType != 'Machine'
Stage 4: where
| where Properties has '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2'
or Properties has '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2'
or Properties has '89e95b76-444d-4c62-991a-0facbeda640c'
Stage 5: project
| project TimeGenerated, Account, Activity, Properties, SubjectLogonId, Computer
Stage 6: join
| join kind=leftouter
(
SecurityEvent
| where EventID == 4624 and LogonType == 3
| where AccountType != 'Machine'
| project TargetLogonId, IpAddress, Computer
)
on $left.SubjectLogonId == $right.TargetLogonId and $left.Computer == $right.Computer
Stage 7: project-reorder
project-reorder
Stage 8: extend (3 consecutive steps)
extend AccountNTDomain, AccountName, DomainIndex, HostName, HostNameDomain
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountType | ne |
| field:"AccountType" kind:ne value:"Machine" |
EventID | eq |
| field:"EventID" kind:eq |
LogonType | eq |
| field:"LogonType" kind:eq value:"3" |
ObjectServer | eq |
| field:"ObjectServer" kind:eq value:"DS" |
Properties | match |
| field:"Properties" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
Activity | project |
Computer | project |
Properties | project |
SubjectLogonId | project |
TimeGenerated | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
OCI - Insecure metadata endpoint
#'Detects insecure metadata endpoint.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 9c4b1b9c-6462-41ce-8f2e-ce8c104331fc
name: OCI - Insecure metadata endpoint
description: |
'Detects insecure metadata endpoint.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: OracleCloudInfrastructureLogsConnector
dataTypes:
- OCILogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1069
query: |
OCILogs
| where data_request_headers_oci_original_url_s contains '/opc/v1' or data_request_headers_oci_original_url_s contains '/openstack'
| where HttpStatusCode == 200
| extend IPCustomEntity = DstIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
OCILogs
Stage 2: where
| where data_request_headers_oci_original_url_s contains '/opc/v1' or data_request_headers_oci_original_url_s contains '/openstack'
Stage 3: where
| where HttpStatusCode == 200
Stage 4: extend
| extend IPCustomEntity = DstIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
HttpStatusCode | eq |
| field:"HttpStatusCode" kind:eq value:"200" |
data_request_headers_oci_original_url_s | contains |
| field:"data_request_headers_oci_original_url_s" kind:contains |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
Palo Alto Prisma Cloud - Multiple failed logins for user
#'Detects multiple failed logins for the same user account.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 4f688252-bf9b-4136-87bf-d540b5be1050
name: Palo Alto Prisma Cloud - Multiple failed logins for user
description: |
'Detects multiple failed logins for the same user account.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: PaloAltoPrismaCloud
dataTypes:
- PaloAltoPrismaCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1110
query: |
let threshold = 10;
PaloAltoPrismaCloud
| where ResourceType =~ 'Login'
| where EventResult =~ 'Failed'
| where EventMessage !has 'access key'
| summarize count() by UserName, bin(TimeGenerated, 5m)
| where count_ > threshold
| extend AccountCustomEntity = UserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
PaloAltoPrismaCloud
Stage 2: where
| where ResourceType =~ 'Login'
Stage 3: where
| where EventResult =~ 'Failed'
Stage 4: where
| where EventMessage !has 'access key'
Stage 5: summarize
| summarize count() by UserName, bin(TimeGenerated, 5m)
Stage 6: where
| where count_ > threshold
Stage 7: extend
| extend AccountCustomEntity = UserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResult | eq |
| field:"EventResult" kind:eq value:"Failed" |
ResourceType | eq |
| field:"ResourceType" kind:eq value:"Login" |
count_ | gt |
| field:"count_" kind:gt value:"10" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
EventMessage | match | access key | excludes:EventMessage field:"EventMessage" value:"access key" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserName | summarize |
AccountCustomEntity | extend |
Pathlock TDnR - SAP Download Observer Events
#Detects download events captured by the Pathlock DownLoad Observer in SAP, forwarded to Microsoft Sentinel. Unusual download patterns may indicate bulk data exfiltration, unauthorized extraction of sensitive SAP data, or insider threat activity involving large-scale data downloads.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c51
name: Pathlock TDnR - SAP Download Observer Events
kind: Scheduled
description: >-
Detects download events captured by the Pathlock DownLoad Observer in SAP, forwarded to
Microsoft Sentinel. Unusual download patterns may indicate bulk data exfiltration, unauthorized
extraction of sensitive SAP data, or insider threat activity involving large-scale data downloads.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1048
query: |
Pathlock_TDnR_CL
| where DataSource == "SAST_DO"
| 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 == "SAST_DO"
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:"SAST_DO" |
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 |
PROD (TM001.1) - GROUP - Added to Group Outside the Object Tier Level
#Added an object to a member of a Tier Model group that is outside of the same Tier. Example a Tier 0 user added to a Tier 1 group.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 5136: A directory service object was modified. |
Rule Body
id: b300c7f8-dc5e-4fc6-879b-63f0013358ce
name: 'PROD (TM001.1) - GROUP - Added to Group Outside the Object Tier Level'
description: |
Added an object to a member of a Tier Model group that is outside of the same Tier. Example a Tier 0 user added to a Tier 1 group.
severity: High
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.002
query: |
SecurityEvent
| where EventID == 5136
and Activity has "modified"
and (EventData matches regex @"(?i)OU=Tier 0 Accounts"
or EventData matches regex @"(?i)OU=Tier 1 Accounts"
or EventData matches regex @"(?i)OU=Tier 2 Accounts"
or EventData matches regex @"(?i)OU=Tier 0 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 1 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 2 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 0 Groups"
or EventData matches regex @"(?i)OU=Tier 1 Groups"
or EventData matches regex @"(?i)OU=Tier 2 Groups"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW")
and EventData has '"AttributeLDAPDisplayName">member'
and EventData has 'OperationType">%%14674'
| extend GroupName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend GroupName = extract("(?i)CN=([^,]+)", 1, GroupName_e)
| where GroupName != "Protected Users"
| where GroupName != "Allowed RODC Password Replication Group"
| extend GroupTier_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend GroupTier = extract("(?i)OU=(Tier [^ ]+)", 1, GroupTier_e)
| extend GroupTier = iff(isempty(GroupTier), "Outside Tier Model", GroupTier)
| extend ObjectName_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
| extend ObjectTier_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectTier = extract("(?i)OU=(Tier [^ ]+)", 1, ObjectTier_e)
| extend ObjectTier = iff(isempty(ObjectTier), "Outside Tier Model", ObjectTier)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))
| where GroupTier != ObjectTier
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectType = AccountType
, GroupName
, GroupTier
, ActivityType
, OperationType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ObjectName
- entityType: SecurityGroup
fieldMappings:
- identifier: DistinguishedName
columnName: GroupName
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
customDetails:
ObjectName: ObjectName
ObjectTier: ObjectTier
ObjectType: ObjectType
GroupName: GroupName
GroupTier: GroupTier
ActivityType: ActivityType
OperationType: OperationType
Account: Account
Domain: Domain
Channel: Channel
EventID: EventID
EventData: EventData
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5M
matchingMethod: Selected
groupByEntities:
- Account
groupByCustomDetails:
- ObjectName
alertDetailsOverride:
alertDisplayNameFormat: '(TM001.1) A {{ObjectTier}} object was added to non-{{ObjectTier}} group {{GroupName}}'
alertDescriptionFormat: |
Tier Model Alert TM001 - A {{ObjectTier}} object was added to non-{{ObjectTier}} group {{GroupName}}.
Resolution - Remove the object from the group
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID == 5136
and Activity has "modified"
and (EventData matches regex @"(?i)OU=Tier 0 Accounts"
or EventData matches regex @"(?i)OU=Tier 1 Accounts"
or EventData matches regex @"(?i)OU=Tier 2 Accounts"
or EventData matches regex @"(?i)OU=Tier 0 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 1 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 2 Service Accounts"
or EventData matches regex @"(?i)OU=Tier 0 Groups"
or EventData matches regex @"(?i)OU=Tier 1 Groups"
or EventData matches regex @"(?i)OU=Tier 2 Groups"
or EventData matches regex @"(?i)OU=Tier 0 PAW"
or EventData matches regex @"(?i)OU=Tier 1 PAW"
or EventData matches regex @"(?i)OU=Tier 2 PAW")
and EventData has '"AttributeLDAPDisplayName">member'
and EventData has 'OperationType">%%14674'
Stage 3: extend
| extend GroupName_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
Stage 4: extend
| extend GroupName = extract("(?i)CN=([^,]+)", 1, GroupName_e)
Stage 5: where
| where GroupName != "Protected Users"
Stage 6: where
| where GroupName != "Allowed RODC Password Replication Group"
Stage 7: extend (11 consecutive steps)
| extend GroupTier_e = extract("<Data Name=\"ObjectDN\">([^<]+)</Data>", 1, EventData)
| extend GroupTier = extract("(?i)OU=(Tier [^ ]+)", 1, GroupTier_e)
| extend GroupTier = iff(isempty(GroupTier), "Outside Tier Model", GroupTier)
| extend ObjectName_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectName = extract("(?i)CN=([^,]+)", 1, ObjectName_e)
| extend ObjectTier_e = extract("<Data Name=\"AttributeValue\">([^<]+)</Data>", 1, EventData)
| extend ObjectTier = extract("(?i)OU=(Tier [^ ]+)", 1, ObjectTier_e)
| extend ObjectTier = iff(isempty(ObjectTier), "Outside Tier Model", ObjectTier)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
| extend ActivityType = extract("([a-zA-Z]+)\\.$", 1, Activity)
| extend OperationType = iff(EventData has 'OperationType">%%14674', "Added", tostring(OperationType))
Stage 8: where
| where GroupTier != ObjectTier
Stage 9: project
| project TimeGenerated
, ObjectName
, ObjectTier
, ObjectType = AccountType
, GroupName
, GroupTier
, ActivityType
, OperationType
, Account
, Domain
, Computer
, Channel
, EventID
, EventData
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Activity | match |
| field:"Activity" kind:match value:"modified" |
EventData | match |
| field:"EventData" kind:match |
EventData | regex_match |
| field:"EventData" kind:regex_match |
EventID | eq |
| field:"EventID" kind:eq value:"5136" |
GroupName | ne |
| field:"GroupName" kind:ne |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
ActivityType | project |
Channel | project |
Computer | project |
Domain | project |
EventData | project |
EventID | project |
GroupName | project |
GroupTier | project |
ObjectName | project |
ObjectTier | project |
ObjectType | project |
OperationType | project |
TimeGenerated | project |
Progress MOVEIt File transfer above threshold
#Identifies Progress MOVEIt File Transfers above certain threshold in a 15min time period. Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| MOVEit-DMZ-Audit | Event ID 0: MOVEit file-transfer audit record |
Rule Body
id: 9bd18b63-f1ca-4375-95db-39fda00bfe20
name: Progress MOVEIt File transfer above threshold
description: |
'Identifies Progress MOVEIt File Transfers above certain threshold in a 15min time period.
Please note that entity mapping for arrays is not supported, so when there is a single value in an array, we will pull that value from the array as a single string to populate the entity to support entity mapping features within Sentinel. Additionally, if the array is multivalued, we will input a string to indicate this with a unique hash so that matching will not occur.'
severity: Medium
requiredDataConnectors:
- connectorId: WindowsForwardedEvents
dataTypes:
- WindowsEvent
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1020
query: |
let threshold = 500;
Event
| where EventID == 0 and Source == "MOVEit DMZ Audit" and EventLog == "Application"
| where RenderedDescription has_any ("Downloaded", "Uploaded")
| parse RenderedDescription with * "User '" userdisplayname "' (" *
| parse RenderedDescription with * "IPAddress: " ipaddress " FileID:" * " FileName: " filename " FolderID: " folderid " FolderPath: " folderpath " Username: " username " AgentBrand: " *
| summarize count_=count(), fileslist=make_set(filename) by Computer,userdisplayname,ipaddress,bin(TimeGenerated, 1d)
| where count_ >= threshold
| extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: userdisplayname
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ipaddress
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Computer
- entityType: File
fieldMappings:
- identifier: Name
columnName: FileSample
customDetails:
TransferCount: count_
FilesList: fileslist
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 5h
matchingMethod: Selected
groupByEntities:
- Account
- Host
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 500;
Stage 1: source
Event
Stage 2: where
| where EventID == 0 and Source == "MOVEit DMZ Audit" and EventLog == "Application"
Stage 3: where
| where RenderedDescription has_any ("Downloaded", "Uploaded")
Stage 4: parse
| parse RenderedDescription with * "User '" userdisplayname "' (" *
Stage 5: parse
| parse RenderedDescription with * "IPAddress: " ipaddress " FileID:" * " FileName: " filename " FolderID: " folderid " FolderPath: " folderpath " Username: " username " AgentBrand: " *
Stage 6: summarize
| summarize count_=count(), fileslist=make_set(filename) by Computer,userdisplayname,ipaddress,bin(TimeGenerated, 1d)
Stage 7: where
| where count_ >= threshold
Stage 8: extend
| extend FileSample = iff(array_length(fileslist) == 1, tostring(fileslist[0]), strcat("SeeFilesListField","_", tostring(hash(tostring(fileslist)))))
FileSample =fileslist == 1tostring(fileslist[0])strcat("SeeFilesListField", "_", tostring(hash(tostring(fileslist))))Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"0" |
EventLog | eq |
| field:"EventLog" kind:eq value:"Application" |
RenderedDescription | match |
| field:"RenderedDescription" kind:match |
count_ | ge |
| field:"count_" kind:ge value:"500" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
count_ | summarize |
fileslist | summarize |
ipaddress | summarize |
userdisplayname | summarize |
FileSample | extend |
TI map Email entity to PaloAlto CommonSecurityLog
#'Identifies a match in CommonSecurityLog table from any Email IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: ffcd575b-3d54-482a-a6d8-d0de13b6ac63
name: TI map Email entity to PaloAlto CommonSecurityLog
description: |
'Identifies a match in CommonSecurityLog table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
ThreatIntelligenceIndicator
//Filtering the table for Email related IOCs
| where isnotempty(EmailSenderAddress)
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
// using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
| join kind=innerunique (
CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
// Filtering PAN Logs for specific event type to match relevant email entities
| where DeviceVendor == "Palo Alto Networks" and DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
| extend DestinationUserID = tolower(DestinationUserID)
| where DestinationUserID matches regex emailregex
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.DestinationUserID
| where CommonSecurityLog_TimeGenerated < ExpirationDateTime
| summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by IndicatorId, DestinationUserID
| project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore, EmailSenderName, EmailRecipient,
EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, DestinationUserID, DeviceEventClassID, LogSeverity, DeviceAction, SourceIP, SourcePort,
DestinationIP, DestinationPort, Protocol, ApplicationProtocol
| extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: DestinationUserID
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.2.6
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
Stage 1: source
ThreatIntelligenceIndicator
Stage 2: where
| where isnotempty(EmailSenderAddress)
Stage 3: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 4: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
Stage 5: where
| where Active == true and ExpirationDateTime > now()
Stage 6: join
| join kind=innerunique (
CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
| where DeviceVendor == "Palo Alto Networks" and DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
| extend DestinationUserID = tolower(DestinationUserID)
| where DestinationUserID matches regex emailregex
| extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.DestinationUserID
Stage 7: where
where CommonSecurityLog_TimeGenerated < ExpirationDateTime
Stage 8: summarize
summarize CommonSecurityLog_TimeGenerated by IndicatorId, DestinationUserID
Stage 9: project
project ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, ConfidenceScore, Description, DestinationIP, DestinationPort, DestinationUserID, DeviceAction, DeviceEventClassID, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, LogSeverity, Protocol, SourceIP, SourcePort, ThreatType, Url
Stage 10: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
ApplicationProtocol | in |
| field:"ApplicationProtocol" kind:in |
DestinationUserID | is_not_null | field:"DestinationUserID" kind:is_not_null | |
DestinationUserID | regex_match |
| field:"DestinationUserID" kind:regex_match value:"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$" |
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"wildfire" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Palo Alto Networks" |
EmailSenderAddress | is_not_null | field:"EmailSenderAddress" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ApplicationProtocol | project |
CommonSecurityLog_TimeGenerated | project |
ConfidenceScore | project |
Description | project |
DestinationIP | project |
DestinationPort | project |
DestinationUserID | project |
DeviceAction | project |
DeviceEventClassID | project |
EmailRecipient | project |
EmailSenderName | project |
EmailSourceDomain | project |
EmailSourceIpAddress | project |
EmailSubject | project |
ExpirationDateTime | project |
FileHashType | project |
FileHashValue | project |
IndicatorId | project |
LogSeverity | project |
Protocol | project |
SourceIP | project |
SourcePort | project |
ThreatType | project |
Url | project |
timestamp | extend |