Detection rules › Kusto
Kusto rules: 3e
Alsid LSASS Memory
#'Searches for OS Credentials dumping attacks'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 3acf5617-7c41-4085-9a79-cc3a425ba83a
name: Alsid LSASS Memory
description: |
'Searches for OS Credentials dumping attacks'
severity: High
requiredDataConnectors:
- connectorId: AlsidForAD
dataTypes:
- AlsidForADLog_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1003.001
query: |
afad_parser
| where MessageType == 2 and Codename == "OS Credential Dumping: LSASS Memory"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "OS Credential Dumping: LSASS Memory"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"OS Credential Dumping: LSASS Memory" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
BloodHound Attack Path Finding - Non-Tier Zero Principal Can Grant Tier Zero Entra ID Role
#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.
BloodHound Attack Path Finding - Non-Tier Zero Principal Can Grant Tier Zero Entra ID Role
Rule Body
id: 146f6a92-d11a-4728-b66d-6d4dc359ff59
name: BloodHound Attack Path Finding - Non-Tier Zero Principal Can Grant Tier Zero
Entra ID Role
description: BloodHound Attack Path Finding - Non-Tier Zero Principal Can Grant Tier
Zero Entra ID Role
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BloodHoundEnterprise
dataTypes:
- BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureT0MGGrantRole\""
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: domain_name
customDetails:
Description: ShortDescription
Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
BHEAttackPathsData_CL
Stage 2: where
| where Finding == "AzureT0MGGrantRole"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"AzureT0MGGrantRole" |
Cognni Incidents for Low Sensitivity Financial Information
#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.
'Display incidents in which low sensitivity financial information was placed at risk by user sharing.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Rule Body
id: 77171efa-4502-4ab7-9d23-d12305ff5a5e
name: Cognni Incidents for Low Sensitivity Financial Information
description: |
'Display incidents in which low sensitivity financial information was placed at risk by user sharing.'
severity: Low
requiredDataConnectors:
- connectorId: CognniSentinelDataConnector
dataTypes:
- CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Collection
relevantTechniques:
- T1530
query: |
let lowRisk = 1;
let financial = 'Financial Information';
CognniIncidents_CL
| where Severity == lowRisk
| where informationType_s == financial
| where TimeGenerated >= ago(5h)
| extend AccountCustomEntity = userId_s
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let lowRisk = 1;
let financial = 'Financial Information';
Stage 1: source
CognniIncidents_CL
Stage 2: where
| where Severity == lowRisk
Stage 3: where
| where informationType_s == financial
Stage 4: where
| where TimeGenerated >= ago(5h)
Stage 5: extend
| extend AccountCustomEntity = userId_s
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Severity | eq |
| field:"Severity" kind:eq value:"1" |
informationType_s | eq |
| field:"informationType_s" kind:eq value:"Financial Information" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Cyble Vision Alerts Domain Expiry Alert
#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.
'Triggers when a monitored domain is about to expire. Expired domains risk service disruption, takeover, or misuse by adversaries.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 81404e99-ce27-45aa-aa19-a276a3d4c645
name: Cyble Vision Alerts Domain Expiry Alert
description: |
'Triggers when a monitored domain is about to expire. Expired domains risk service disruption, takeover, or misuse by adversaries.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
dataTypes:
- CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics:
- Impact
relevantTechniques:
- T1499
query: |
Alerts_domain_expiry
| where Service == "domain_expiry"
| extend MappedSeverity = Severity
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
alertDetailsOverride:
alertDisplayNameFormat: Domain Expiration Imminent {{DE_Domain}}
alertDescriptionFormat: |
The monitored domain is expiring soon. Expiry Date {{DE_ExpiryDate}}. Due in {{DE_DueDays}} days. Alert Type {{DE_TypeText}}. Immediate renewal is recommended to avoid service disruption or domain hijacking.
customDetails:
MappedSeverity: Severity
Status: Status
AlertID: AlertID
Service: Service
DE_Domain: DE_Domain
DE_DueDays: DE_DueDays
DE_ExpiryDate: DE_ExpiryDate
DE_TypeText: DE_TypeText
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DE_Domain
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Alerts_domain_expiry
Stage 2: where
| where Service == "domain_expiry"
Stage 3: extend
| extend MappedSeverity = Severity
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Service | eq |
| field:"Service" kind:eq value:"domain_expiry" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MappedSeverity | extend |
CYFIRMA - Medium severity Malicious Phishing Network Indicators - Block Recommended 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 analytics rule identifies network indicators such as URLs, IP addresses, and domains related to phishing campaigns, as reported by CYFIRMA threat intelligence. These indicators are flagged with a recommended action to block and are categorized under the 'Phishing' role. Such infrastructure is often used to deliver phishing emails, host fake login portals, or redirect victims to credential-harvesting pages. Blocking these indicators proactively helps prevent user compromise and data theft."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Credential Access | |
| Exfiltration |
Rule Body
id: 5468e012-6681-44fb-be2d-b1cd58b62ac7
name: CYFIRMA - Medium severity Malicious Phishing Network Indicators - Block Recommended Rule
description: |
"This analytics rule identifies network indicators such as URLs, IP addresses, and domains related to phishing campaigns, as reported by CYFIRMA threat intelligence.
These indicators are flagged with a recommended action to block and are categorized under the 'Phishing' role.
Such infrastructure is often used to deliver phishing emails, host fake login portals, or redirect victims to credential-harvesting pages.
Blocking these indicators proactively helps prevent user compromise and data theft."
version: 1.0.1
kind: Scheduled
severity: Medium
enabled: false
requiredDataConnectors:
- connectorId: CyfirmaCyberIntelligenceDC
dataTypes:
- CyfirmaIndicators_CL
query: |
//Malicious Phishing Network Indicators - Block Recommended
let timeFrame= 5m;
CyfirmaIndicators_CL
| where (ConfidenceScore < 80 and ConfidenceScore >= 50)
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'Phishing'
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
| mv-expand extensionKeys
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
- InitialAccess
- Execution
- CredentialAccess
- Exfiltration
relevantTechniques:
- T1566
- T1204
- T1556
- T1110
- T1041
- T1566.001
- T1566.002
- T1204.001
- T1556.002
- T1110.003
alertDetailsOverride:
alertDisplayNameFormat: "High-Confidence Malicious Phishing Network Indicators - Block Recommended - {{name}} "
alertDescriptionFormat: "{{Description}} - {{name}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
customDetails:
ThreatActors: ThreatActors
Sources: Sources
RecommendedActions: RecommendedActions
Roles: Roles
Country: Country
Description: Description
ConfidenceScore: ConfidenceScore
SecurityVendors: SecurityVendors
IndicatorID: IndicatorID
Created: created
Modified: modified
ValidFrom: valid_from
Tags: Tags
ThreatType: ThreatType
TimeGenerated: TimeGenerated
IPAbuse: IPAbuse
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv4
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPv6
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: URL
fieldMappings:
- identifier: Url
columnName: URL
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
CyfirmaIndicators_CL
Stage 2: where
| where (ConfidenceScore < 80 and ConfidenceScore >= 50)
and TimeGenerated between (ago(timeFrame) .. now())
and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'Phishing'
Stage 3: extend (6 consecutive steps)
| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)
Stage 4: mv-expand
| mv-expand extensionKeys
Stage 5: extend (4 consecutive steps)
| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend
extension_id = extensionKeyStr,
ASN_Owner = props.asn_owner,
ASN = props.asn,
ProviderName = 'CYFIRMA',
ProductName = 'DeCYFIR/DeTCT'
Stage 6: project
| project
IPv4,
IPv6,
URL,
Domain,
ThreatActors,
RecommendedActions,
Sources,
Roles,
Country,
IPAbuse,
name,
Description,
ConfidenceScore,
IndicatorID,
created,
modified,
valid_from,
Tags,
ThreatType,
TimeGenerated,
SecurityVendors,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
Dataverse - Organization settings modified
#Identifies changes made at organization level in the Dataverse environment.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft 365 | any: Dataverse and model-driven apps audit (catch-all) |
Rule Body
id: a6f6b734-3db8-4259-a988-69e0b8eac0c2
kind: Scheduled
name: Dataverse - Organization settings modified
description: Identifies changes made at organization level in the Dataverse environment.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Dataverse
dataTypes:
- DataverseActivity
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1078
query: |
let query_frequency = 1h;
DataverseActivity
| where TimeGenerated >= ago(query_frequency)
| where Message == "Update" and EntityName == "organization"
| mv-expand Fields
| extend FieldName = tostring(Fields.Name)
| extend Value = tostring(Fields.Value)
| where FieldName != "organizationid"
| lookup MSBizAppsOrgSettings on FieldName
| extend
CloudAppId = int(32780),
AccountName = tostring(split(UserId, '@')[0]),
UPNSuffix = tostring(split(UserId, '@')[1])
| project
TimeGenerated,
UserId,
ClientIp,
FieldName,
Value,
DisplayName,
Description,
InstanceUrl,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: AlertPerResult
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
- identifier: InstanceName
columnName: InstanceUrl
alertDetailsOverride:
alertDisplayNameFormat: 'Dataverse - {{DisplayName}} changed in {{InstanceUrl}} '
alertDescriptionFormat: 'Organization setting {{DisplayName}} : {{Description}}
changed by {{UserId}}'
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 1h;
Stage 1: source
DataverseActivity
Stage 2: where
| where TimeGenerated >= ago(query_frequency)
Stage 3: where
| where Message == "Update" and EntityName == "organization"
Stage 4: mv-expand
| mv-expand Fields
Stage 5: extend
| extend FieldName = tostring(Fields.Name)
Stage 6: extend
| extend Value = tostring(Fields.Value)
Stage 7: where
| where FieldName != "organizationid"
Stage 8: kusto:lookup
| lookup MSBizAppsOrgSettings on FieldName
Stage 9: extend
extend AccountName, CloudAppId, UPNSuffix
Stage 10: project
project AccountName, ClientIp, CloudAppId, Description, DisplayName, FieldName, InstanceUrl, TimeGenerated, UPNSuffix, UserId, Value
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EntityName | eq |
| field:"EntityName" kind:eq value:"organization" |
FieldName | ne |
| field:"FieldName" kind:ne value:"organizationid" |
Message | eq |
| field:"Message" kind:eq value:"Update" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
ClientIp | project |
CloudAppId | project |
Description | project |
DisplayName | project |
FieldName | project |
InstanceUrl | project |
TimeGenerated | project |
UPNSuffix | project |
UserId | project |
Value | project |
Detect suspicious conditional access policy modifications
#This detection rule flags events where conditional access policies are getting weaker, when modifications to CA inclusion or exclusion groups are happening, or when the effectiveness of a policy is disabled.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Defense Impairment | |
| Credential Access |
References
Telemetry Coverage
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
// !! TO DO: CHANGE TO YOUR CA GROUP NAMING CONVENTION !!
let ca_include_naming_convention = "CA-Include";
let ca_exclude_naming_convention = "CA-Exclude";
// OPTIONAL - Get PIM activations with justifications for CA changes
let ca_pim_activations = AuditLogs
// Get PIM activations
| where TimeGenerated > ago(24h)
| where OperationName contains "completed (PIM activation)"
// Parse details
| parse AdditionalDetails with * "{\"key\":\"StartTime\",\"value\":\"" PimStartTime "\"" * "{\"key\":\"ExpirationTime\",\"value\":\"" PimExpirationTime "\"" * "{\"key\":\"Justification\",\"value\":\"" PimJustification "\"" *
// Only get CA related PIM justifications
| where PimJustification has_any ("Conditional Access", "CA", "Trusted", "Named", "Location")
// Extend and projects
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| project OperationName, PimJustification, PimStartTime, PimExpirationTime, UserPrincipalName;
// Get suspicious policy changes
let policy_changes = AuditLogs
// Get CA updates
| where TimeGenerated > ago(24h)
| where OperationName in ("Update conditional access policy", "Delete conditional access policy")
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Save the new and old values
| extend NewValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).conditions
| extend OldValueConditions = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).conditions
| extend NewValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).grantControls
| extend OldValueGrandControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).grantControls
| extend NewValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).sessionControls
| extend OldValueSessionControls = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).sessionControls
| extend NewState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).state
| extend OldState = parse_json(tostring(parse_json(TargetResources_modifiedProperties.oldValue))).state
// Count the new inlude arrays
| extend CountNewUserIncludes = array_length(NewValueConditions.users.includeUsers),
CountNewRoleIncludes = array_length(NewValueConditions.users.includeRoles),
CountNewGroupIncludes = array_length(NewValueConditions.users.includeGroups),
CountNewUserActionIncludes = array_length(NewValueConditions.applications.inlcudeUserActions),
CountNewAuthContextIncludes = array_length(NewValueConditions.applications.includeAuthenticationContextClassReferences),
CountNewApplicationIncludes = array_length(NewValueConditions.applications.inlcudeApplications),
CountNewLocationIncludes = array_length(NewValueConditions.locations.includeLocations),
CountNewPlatformIncludes = array_length(NewValueConditions.platforms.includePlatforms)
// Count the old inlude arrays
| extend CountOldUserIncludes = array_length(OldValueConditions.users.includeUsers),
CountOldRoleIncludes = array_length(OldValueConditions.users.includeRoles),
CountOldGroupIncludes = array_length(OldValueConditions.users.includeGroups),
CountOldUserActionIncludes = array_length(OldValueConditions.applications.inlcudeUserActions),
CountOldAuthContextIncludes = array_length(OldValueConditions.applications.includeAuthenticationContextClassReferences),
CountOldApplicationIncludes = array_length(OldValueConditions.applications.inlcudeApplications),
CountOldLocationIncludes = array_length(OldValueConditions.locations.includeLocations),
CountOldPlatformIncludes = array_length(OldValueConditions.platforms.includePlatforms)
// Count the new exclude arrays
| extend CountNewUserExcludes = array_length(NewValueConditions.users.excludeUsers),
CountNewRoleExcludes = array_length(NewValueConditions.users.excludeRoles),
CountNewGroupExcludes = array_length(NewValueConditions.users.excludeGroups),
CountNewApplicationExcludes = array_length(NewValueConditions.applications.excludeApplications),
CountNewLocationExcludes = array_length(NewValueConditions.locations.excludeLocations),
CountNewPlatformExcludes = array_length(NewValueConditions.platforms.excludePlatforms)
// Count the old exclude arrays
| extend CountOldUserExcludes = array_length(OldValueConditions.users.excludeUsers),
CountOldRoleExcludes = array_length(OldValueConditions.users.excludeRoles),
CountOldGroupExcludes = array_length(OldValueConditions.users.excludeGroups),
CountOldApplicationExcludes = array_length(OldValueConditions.applications.excludeApplications),
CountOldLocationExcludes = array_length(OldValueConditions.locations.excludeLocations),
CountOldPlatformExcludes = array_length(OldValueConditions.platforms.excludePlatforms)
// Alert when includes are taken away and excludes are added, application filter changes, or AppType changes
| extend Reasons = dynamic([])
| extend Reasons = iff(CountNewUserIncludes < CountOldUserIncludes, array_concat(Reasons, dynamic(["User removed from include"])), Reasons)
| extend Reasons = iff(CountNewRoleIncludes < CountOldRoleIncludes, array_concat(Reasons, dynamic(["Role removed from include"])), Reasons)
| extend Reasons = iff(CountNewGroupIncludes < CountOldGroupIncludes, array_concat(Reasons, dynamic(["Group removed from include"])), Reasons)
| extend Reasons = iff(CountNewUserExcludes > CountOldUserExcludes, array_concat(Reasons, dynamic(["User added to exclude"])), Reasons)
| extend Reasons = iff(CountNewRoleExcludes > CountOldRoleExcludes, array_concat(Reasons, dynamic(["Role added to exclude"])), Reasons)
| extend Reasons = iff(CountNewGroupExcludes > CountOldGroupExcludes, array_concat(Reasons, dynamic(["Group added to exclude"])), Reasons)
| extend Reasons = iff(CountNewUserActionIncludes < CountOldUserActionIncludes, array_concat(Reasons, dynamic(["User action removed from include"])), Reasons)
| extend Reasons = iff(CountNewAuthContextIncludes < CountOldAuthContextIncludes, array_concat(Reasons, dynamic(["Authentication context removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationIncludes < CountOldApplicationIncludes, array_concat(Reasons, dynamic(["Application removed from include"])), Reasons)
| extend Reasons = iff(CountNewApplicationExcludes > CountOldApplicationExcludes, array_concat(Reasons, dynamic(["Application added to exclude"])), Reasons)
| extend Reasons = iff(CountNewLocationIncludes < CountOldLocationIncludes, array_concat(Reasons, dynamic(["Locations removed from include"])), Reasons)
| extend Reasons = iff(CountNewLocationExcludes > CountOldLocationExcludes, array_concat(Reasons, dynamic(["Locations added to exclude"])), Reasons)
| extend Reasons = iff(CountNewPlatformIncludes < CountOldPlatformIncludes, array_concat(Reasons, dynamic(["Platforms removed from include"])), Reasons)
| extend Reasons = iff(CountNewPlatformExcludes > CountOldPlatformExcludes, array_concat(Reasons, dynamic(["Platforms added to exclude"])), Reasons)
// Flag general changes
| extend Reasons = iff(tostring(NewValueConditions.applications.applicationFilter) != tostring(OldValueConditions.applications.applicationFilter), array_concat(Reasons, dynamic(["Application filter changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.clientAppTypes) != tostring(OldValueConditions.clientAppTypes), array_concat(Reasons, dynamic(["Client app type changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.userRiskLevels) != tostring(OldValueConditions.userRiskLevels), array_concat(Reasons, dynamic(["User risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.signInRiskLevels) != tostring(OldValueConditions.signInRiskLevels), array_concat(Reasons, dynamic(["Sign-in risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.servicePrincipalRiskLevels) != tostring(OldValueConditions.servicePrincipalRiskLevels), array_concat(Reasons, dynamic(["Service Principal risk levels changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueGrandControls) != tostring(OldValueGrandControls), array_concat(Reasons, dynamic(["Grant controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueSessionControls) != tostring(OldValueSessionControls), array_concat(Reasons, dynamic(["Session controls changed"])), Reasons)
| extend Reasons = iff(tostring(NewValueConditions.devices) != tostring(OldValueConditions.devices), array_concat(Reasons, dynamic(["Device conditions changed"])), Reasons)
// Flag Change from include 'all' to only include specifics (since this can evade the count detections)
| extend Reasons = iff(tostring(OldValueConditions.locations.includeLocations) contains "all" and tostring(NewValueConditions.locations.includeLocations) !contains "all", array_concat(Reasons, dynamic(["Include locations changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.platforms.includePlatforms) contains "all" and tostring(NewValueConditions.platforms.includePlatforms) !contains "all", array_concat(Reasons, dynamic(["Include platforms changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.users.includeUsers) contains "all" and tostring(NewValueConditions.users.includeUsers) !contains "all", array_concat(Reasons, dynamic(["Include users changed from all to specific"])), Reasons)
| extend Reasons = iff(tostring(OldValueConditions.applications.includeApplications) contains "all" and tostring(NewValueConditions.applications.includeApplications) !contains "all", array_concat(Reasons, dynamic(["Include applications changed from all to specific"])), Reasons)
// Flag state change to inactive
| extend Reasons = iff(tostring(OldState) == "enabled" and tostring(NewState) != "enabled", array_concat(Reasons, dynamic(["Policy was disabled"])), Reasons)
// Flag policy deletion
| extend Reasons = iff(OperationName == "Delete conditional access policy", array_concat(Reasons, dynamic(["Policy was deleted"])), Reasons);
// Get trusted named location changes
let named_locations = AuditLogs
// Get named location changes
| where TimeGenerated > ago(24h)
| where OperationName in ("Add named location", "Update named location")
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Always flag when the named location is trusted
| extend NewValueIsTrusted = parse_json(tostring(parse_json(TargetResources_modifiedProperties.newValue))).isTrusted
| where NewValueIsTrusted == "true"
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = iff(OperationName == "Add named location", array_concat(Reasons, dynamic(["Trusted named location was added"])), Reasons)
| extend Reasons = iff(OperationName == "Update named location", array_concat(Reasons, dynamic(["Trusted named location was updated"])), Reasons);
// Get changes to groups used in CA policies
let remove_from_include_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Remove member from group"
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Search for the display name of the edited group and find groups with CA naming convention
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_include_naming_convention
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member removed from include group used in CA policy"]);
let add_to_exclude_group = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName == "Add member to group"
// Expand Target resources and the modified properties
| mv-expand TargetResources
| mv-expand TargetResources.modifiedProperties
// Search for the display name of the edited group and find groups with CA naming convention
| where TargetResources_modifiedProperties.displayName == "Group.DisplayName" and TargetResources_modifiedProperties contains ca_exclude_naming_convention
// Add reason
| extend Reasons = dynamic([])
| extend Reasons = dynamic(["Member added to exclude group used in CA policy"]);
// Union all detections
union policy_changes, named_locations, remove_from_include_group, add_to_exclude_group
// Check if reason array is empty
| where Reasons != "[]"
// Sorting and project
| sort by TimeGenerated desc
| project TimeGenerated, OperationName, InitiatedBy, LoggedByService, Result, TargetResources, AADOperationType, Reasons
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
// Look for PIM activations from the same user who performed changes
| join kind=leftouter ca_pim_activations on UserPrincipalName
| project-away UserPrincipalName1
// Check if PIM was justified for user, and only show non-justified PIMs
| extend JustifiedPIM = iff(isnotempty(PimStartTime) and isnotempty(PimExpirationTime) and TimeGenerated between (todatetime(PimStartTime) .. todatetime(PimExpirationTime)), true, false)
| where JustifiedPIM == false
Stages and Predicates
Parameters
let ca_include_naming_convention = "CA-Include";
let ca_exclude_naming_convention = "CA-Exclude";
Let Binding: ca_pim_activations
let ca_pim_activations = AuditLogs
| where TimeGenerated > ago(24h)
| where OperationName contains "completed (PIM activation)"
| parse AdditionalDetails with * "{\"key\":\"StartTime\",\"value\":\"" PimStartTime "\"" * "{\"key\":\"ExpirationTime\",\"value\":\"" PimExpirationTime "\"" * "{\"key\":\"Justification\",\"value\":\"" PimJustification "\"" *
| where PimJustification has_any ("Conditional Access", "CA", "Trusted", "Named", "Location")
| extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| project OperationName, PimJustification, PimStartTime, PimExpirationTime, UserPrincipalName;
Union (4 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: policy_changes, named_locations, remove_from_include_group, add_to_exclude_group
Leg 1: policy_changes
Leg 2: named_locations
Leg 3: remove_from_include_group
Leg 4: add_to_exclude_group
Applied to the Combined Result
| where Reasons != "[]" | sort by TimeGenerated desc | project TimeGenerated, OperationName, InitiatedBy, LoggedByService, Result, TargetResources, AADOperationType, Reasons | extend UserPrincipalName = tostring(InitiatedBy.user.userPrincipalName) | join kind=leftouter ca_pim_activations on UserPrincipalName | project-away UserPrincipalName1 | extend JustifiedPIM = iff(isnotempty(PimStartTime) and isnotempty(PimExpirationTime) and TimeGenerated between (todatetime(PimStartTime) .. todatetime(PimExpirationTime)), true, false) | where JustifiedPIM == false
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
JustifiedPIM | eq |
| field:"JustifiedPIM" kind:eq value:"false" |
NewValueIsTrusted | eq |
| field:"NewValueIsTrusted" kind:eq value:"true" |
OperationName | contains |
| field:"OperationName" kind:contains value:"completed (PIM activation)" |
OperationName | eq |
| field:"OperationName" kind:eq |
OperationName | in |
| field:"OperationName" kind:in |
PimJustification | match |
| field:"PimJustification" kind:match |
Reasons | ne |
| field:"Reasons" kind:ne value:"[]" |
TargetResources_modifiedProperties | contains |
| field:"TargetResources_modifiedProperties" kind:contains |
TargetResources_modifiedProperties.displayName | eq |
| field:"TargetResources_modifiedProperties.displayName" kind:eq value:"Group.DisplayName" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AADOperationType | project |
InitiatedBy | project |
LoggedByService | project |
OperationName | project |
Reasons | project |
Result | project |
TargetResources | project |
TimeGenerated | project |
UserPrincipalName | extend |
JustifiedPIM | extend |
F&O - Unusual sign-in activity using single factor authentication
#Identifies sucessful sign-in events to Finance & Operations and Lifecycle Services using single factor/password authentication. Sign-in events from tenants not using MFA, coming from a Microsoft Entra trusted network location, or from geolocations seen previously in the last 14 days are excluded.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
- Cisco - firewall block but success logon to Microsoft Entra ID (Kusto)
Rule Body
id: 919e939f-95e2-4978-846e-13a721c89ea1
kind: Scheduled
name: F&O - Unusual sign-in activity using single factor authentication
description: Identifies sucessful sign-in events to Finance & Operations and Lifecycle
Services using single factor/password authentication. Sign-in events from tenants
not using MFA, coming from a Microsoft Entra trusted network location, or from geolocations
seen previously in the last 14 days are excluded.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- InitialAccess
relevantTechniques:
- T1552
- T1078
query: |
// Dynamics Lifecycle services: 913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0
// Microsoft Dynamics ERP: 00000015-0000-0000-c000-000000000000
let appid_list = dynamic(["913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0", "00000015-0000-0000-c000-000000000000"]);
let query_frequency = 1h;
let query_lookback = 14d;
let historical_sign_in_activity = SigninLogs
| where TimeGenerated between (ago(query_lookback) .. ago(query_frequency));
let historical_sign_in_locations = historical_sign_in_activity
| summarize by Location;
let multifactor_sign_in_count = toscalar(historical_sign_in_activity
| where AppId in (appid_list) and ResultType == 0
| where AuthenticationRequirement == "multiFactorAuthentication"
| summarize count());
SigninLogs
| where TimeGenerated >= ago(query_frequency)
| where AppId in (appid_list) and ResultType == 0
| where multifactor_sign_in_count > 0
| where Location !in (historical_sign_in_locations)
| where NetworkLocationDetails !has "trustedNamedLocation"
| summarize by UserPrincipalName, AppDisplayName, IPAddress, Location
| extend
CloudAppId = 32780,
AccountName = tostring(split(UserPrincipalName, "@")[0]),
UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
| project
UserPrincipalName,
AppDisplayName,
IPAddress,
Location,
CloudAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: CloudAppId
alertDetailsOverride:
alertDisplayNameFormat: Dynamics 365 F&O - Unusual sign-in without multi-factor
authentication
alertDescriptionFormat: Successful sign in by {{UserPrincipalName}} to {{AppDisplayName}}
from location {{Location}} which has not been seen before in the last 14 days.
version: 3.2.0
Stages and Predicates
Parameters
let appid_list = dynamic(["913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0", "00000015-0000-0000-c000-000000000000"]);
let query_frequency = 1h;
let query_lookback = 14d;
Let Binding: historical_sign_in_activity
let historical_sign_in_activity = SigninLogs
| where TimeGenerated between (ago(query_lookback) .. ago(query_frequency));
Let Binding: historical_sign_in_locations
let historical_sign_in_locations = historical_sign_in_activity
| summarize by Location;
Let Binding: multifactor_sign_in_count
let multifactor_sign_in_count = toscalar(historical_sign_in_activity
| where AppId in (appid_list) and ResultType == 0
| where AuthenticationRequirement == "multiFactorAuthentication"
| summarize count());
Stage 1: source
SigninLogs
Stage 2: where
| where TimeGenerated >= ago(query_frequency)
Stage 3: where
| where AppId in (appid_list) and ResultType == 0
Stage 4: where
| where multifactor_sign_in_count > 0
Stage 5: where
| where Location !in (historical_sign_in_locations)
Stage 6: where
| where NetworkLocationDetails !has "trustedNamedLocation"
Stage 7: summarize
| summarize by UserPrincipalName, AppDisplayName, IPAddress, Location
Stage 8: extend
| extend
CloudAppId = 32780,
AccountName = tostring(split(UserPrincipalName, "@")[0]),
UPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Stage 9: project
| project
UserPrincipalName,
AppDisplayName,
IPAddress,
Location,
CloudAppId,
AccountName,
UPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AppId | in |
| field:"AppId" kind:in |
ResultType | eq |
| field:"ResultType" kind:eq value:"0" |
multifactor_sign_in_count | gt |
| field:"multifactor_sign_in_count" kind:gt value:"0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Location | eq | historical_sign_in_locations | excludes:Location field:"Location" value:"historical_sign_in_locations" |
NetworkLocationDetails | match | trustedNamedLocation | excludes:NetworkLocationDetails field:"NetworkLocationDetails" value:"trustedNamedLocation" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AppDisplayName | project |
CloudAppId | project |
IPAddress | project |
Location | project |
UPNSuffix | project |
UserPrincipalName | project |
Filewall - Blocked files
#Identifies files blocked by Filewall for Microsoft 365 (SharePoint/OneDrive/Teams).
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 86e7f6fd-5c29-4a3a-bced-3eca3fb0c621
name: Filewall - Blocked files
description: Identifies files blocked by Filewall for Microsoft 365 (SharePoint/OneDrive/Teams).
severity: High
status: Available
kind: Scheduled
query: |
FilewallM365FileEvent()
| where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'
| where EventType == 'FileBlocked'
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
relevantTechniques:
- T1048
tactics:
- Exfiltration
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
matchingMethod: AllEntities
reopenClosedIncident: false
enabled: false
lookbackDuration: PT1H
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: File
fieldMappings:
- identifier: Name
columnName: TargetFileName
- identifier: Directory
columnName: TargetFilePath
requiredDataConnectors:
- dataTypes:
- FilewallFile_CL
connectorId: FilewallM365
version: 1.0.0
Stages and Predicates
Stage 1: source
FilewallM365FileEvent()
Stage 2: where
| where EventVendor == 'ODI-X' and EventProduct == 'Filewall for Microsoft 365'
Stage 3: where
| where EventType == 'FileBlocked'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventProduct | eq |
| field:"EventProduct" kind:eq value:"Filewall for Microsoft 365" |
EventType | eq |
| field:"EventType" kind:eq value:"FileBlocked" |
EventVendor | eq |
| field:"EventVendor" kind:eq value:"ODI-X" |
Flare paste results
#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 searches for events found on code Snippet (paste) sharing platform.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: 9cb7c337-f177-4af6-b0e8-b6b7552d762d
name: Flare paste results
description: |
'This query searches for events found on code Snippet (paste) sharing platform.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Flare
dataTypes:
- FireworkV2_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1593
query: |
FireworkV2_CL
| where notempty(uid) and RiskScore >= 3
| extend index_name = split(uid, "/")[0]
| where index_name == "paste"
version: 3.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
FireworkV2_CL
Stage 2: where
| where notempty(uid) and RiskScore >= 3
Stage 3: extend
| extend index_name = split(uid, "/")[0]
Stage 4: where
| where index_name == "paste"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RiskScore | ge |
| field:"RiskScore" kind:ge value:"3" |
index_name | eq |
| field:"index_name" kind:eq value:"paste" |
uid | is_not_null | field:"uid" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
index_name | extend |
High Urgency IONIX Action Items
#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 alert for active IONIX Action Items with high urgency (9-10). Urgency can be altered using the "min_urgency" variable in the query.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 8e0403b1-07f8-4865-b2e9-74d1e83200a4
name: High Urgency IONIX Action Items
description: |
'This query creates an alert for active IONIX Action Items with high urgency (9-10).
Urgency can be altered using the "min_urgency" variable in the query.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CyberpionSecurityLogs
dataTypes:
- CyberpionActionItems_CL
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1195
query: |
let timeframe = 14d;
let min_urgency = 9;
CyberpionActionItems_CL
| where TimeGenerated > ago(timeframe)
| summarize arg_max(TimeGenerated, *) by id_s
| where is_open_b == true
| where urgency_d >= min_urgency
| extend timestamp = opening_datetime_t
| extend DNSCustomEntity = host_s
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DNSCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 14d;
let min_urgency = 9;
Stage 1: source
CyberpionActionItems_CL
Stage 2: where
| where TimeGenerated > ago(timeframe)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by id_s
Stage 4: where
| where is_open_b == true
Stage 5: where
| where urgency_d >= min_urgency
Stage 6: extend
| extend timestamp = opening_datetime_t
Stage 7: extend
| extend DNSCustomEntity = host_s
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
is_open_b | eq |
| field:"is_open_b" kind:eq value:"true" |
urgency_d | ge |
| field:"urgency_d" kind:ge value:"9" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
id_s | summarize |
timestamp | extend |
DNSCustomEntity | extend |
Known Forest Blizzard group domains - July 2019
#Matches domain name IOCs related to Forest Blizzard group activity published July 2019 with CommonSecurityLog, DnsEvents and VMConnection dataTypes. References: https://blogs.microsoft.com/on-the-issues/2019/07/17/new-cyberthreats-require-new-ways-to-protect-democracy/.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 074ce265-f684-41cd-af07-613c5f3e6d0d
name: Known Forest Blizzard group domains - July 2019
description: |
'Matches domain name IOCs related to Forest Blizzard group activity published July 2019 with CommonSecurityLog, DnsEvents and VMConnection dataTypes.
References: https://blogs.microsoft.com/on-the-issues/2019/07/17/new-cyberthreats-require-new-ways-to-protect-democracy/.'
severity: High
tags:
- Schema: ASIMDns
SchemaVersion: 0.1.1
requiredDataConnectors:
- connectorId: DNS
dataTypes:
- DnsEvents
- connectorId: AzureMonitor(VMInsights)
dataTypes:
- VMConnection
- connectorId: CiscoASA
dataTypes:
- CommonSecurityLog
- connectorId: PaloAltoNetworks
dataTypes:
- CommonSecurityLog
- connectorId: AzureFirewall
dataTypes:
- AzureDiagnostics
- AZFWApplicationRule
- AZFWDnsQuery
- connectorId: Zscaler
dataTypes:
- CommonSecurityLog
- connectorId: InfobloxNIOS
dataTypes:
- Syslog
- connectorId: GCPDNSDataConnector
dataTypes:
- GCP_DNS_CL
- connectorId: NXLogDnsLogs
dataTypes:
- NXLog_DNS_Server_CL
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_dns_CL
- connectorId: Corelight
dataTypes:
- Corelight_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let DomainNames = dynamic(["irf.services","microsoft-onthehub.com","msofficelab.com","com-mailbox.com","my-sharefile.com","my-sharepoints.com",
"accounts-web-mail.com","customer-certificate.com","session-users-activities.com","user-profile-credentials.com","verify-linke.com","support-servics.net",
"onedrive-sharedfile.com","onedrv-live.com","transparencyinternational-my-sharepoint.com","transparencyinternational-my-sharepoints.com","soros-my-sharepoint.com"]);
(union isfuzzy=true
(CommonSecurityLog
| where Message has_any (DomainNames)
| parse Message with * '(' DNSName ')' *
| extend AccountName = SourceUserID, DeviceName, IPAddress = SourceIP
),
(_Im_Dns(domain_has_any=DomainNames)
| where DnsQuery has_any (DomainNames)
| extend IPAddress = SrcIpAddr, DeviceName = Dvc
),
(VMConnection
| where RemoteDnsCanonicalNames has_any (DomainNames)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| extend IPAddress = RemoteIp, DeviceName = Computer
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where DestinationHost has_any (DomainNames)
| extend DNSName = DestinationHost
| extend IPAddress = SourceHost
),
(AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallDnsProxy"
| project TimeGenerated,Resource, msg_s, Type
| parse msg_s with "DNS Request: " ClientIP ":" ClientPort " - " QueryID " " Request_Type " " Request_Class " " Request_Name ". " Request_Protocol " " Request_Size " " EDNSO_DO " " EDNS0_Buffersize " " Responce_Code " " Responce_Flags " " Responce_Size " " Response_Duration
| where Request_Name has_any (DomainNames)
| extend DNSName = Request_Name
| extend IPAddress = ClientIP
),
(AZFWApplicationRule
| where isnotempty(Fqdn)
| where Fqdn has_any (DomainNames)
| extend DNSName = Fqdn
| extend IPAddress = SourceIp
),
(AZFWDnsQuery
| where isnotempty(QueryName)
| where QueryName has_any (DomainNames)
| extend DNSName = QueryName
| extend IPAddress = SourceIp
),
(
_Im_WebSession(url_has_any=DomainNames)
| extend IPAddress=IpAddr, DeviceName=Hostname, AccountName = tostring(split(User, "@")[0]), AccountDomain = tostring(split(User, "@")[1])
)
)
| where DNSName has_any (DomainNames)
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: User
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: HostNameDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.6.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - 0-day Vulnerability" ]
Stages and Predicates
Let Binding: DomainNames
let DomainNames = dynamic(["irf.services","microsoft-onthehub.com","msofficelab.com","com-mailbox.com","my-sharefile.com","my-sharepoints.com",
"accounts-web-mail.com","customer-certificate.com","session-users-activities.com","user-profile-credentials.com","verify-linke.com","support-servics.net",
"onedrive-sharedfile.com","onedrv-live.com","transparencyinternational-my-sharepoint.com","transparencyinternational-my-sharepoints.com","soros-my-sharepoint.com"]);
Union isfuzzy=true (8 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: CommonSecurityLog, _Im_Dns, VMConnection, AzureDiagnostics, AzureDiagnostics, AZFWApplicationRule, AZFWDnsQuery, _Im_WebSession
Leg 1: CommonSecurityLog
CommonSecurityLog
| where Message has_any (DomainNames)
| parse Message with * '(' DNSName ')' *
| extend AccountName = SourceUserID, DeviceName, IPAddress = SourceIP
Leg 2: _Im_Dns
_Im_Dns(domain_has_any=DomainNames)
| where DnsQuery has_any (DomainNames)
| extend IPAddress = SrcIpAddr, DeviceName = Dvc
Leg 3: VMConnection
VMConnection
| where RemoteDnsCanonicalNames has_any (DomainNames)
| parse RemoteDnsCanonicalNames with * '["' DNSName '"]' *
| extend IPAddress = RemoteIp, DeviceName = Computer
Leg 4: AzureDiagnostics
AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallApplicationRule"
| parse msg_s with Protocol 'request from ' SourceHost ':' SourcePort 'to ' DestinationHost ':' DestinationPort '. Action:' Action
| where DestinationHost has_any (DomainNames)
| extend DNSName = DestinationHost
| extend IPAddress = SourceHost
Leg 5: AzureDiagnostics
AzureDiagnostics
| where ResourceType == "AZUREFIREWALLS"
| where Category == "AzureFirewallDnsProxy"
| project TimeGenerated,Resource, msg_s, Type
| parse msg_s with "DNS Request: " ClientIP ":" ClientPort " - " QueryID " " Request_Type " " Request_Class " " Request_Name ". " Request_Protocol " " Request_Size " " EDNSO_DO " " EDNS0_Buffersize " " Responce_Code " " Responce_Flags " " Responce_Size " " Response_Duration
| where Request_Name has_any (DomainNames)
| extend DNSName = Request_Name
| extend IPAddress = ClientIP
Leg 6: AZFWApplicationRule
AZFWApplicationRule
| where isnotempty(Fqdn)
| where Fqdn has_any (DomainNames)
| extend DNSName = Fqdn
| extend IPAddress = SourceIp
Leg 7: AZFWDnsQuery
AZFWDnsQuery
| where isnotempty(QueryName)
| where QueryName has_any (DomainNames)
| extend DNSName = QueryName
| extend IPAddress = SourceIp
Leg 8: _Im_WebSession
_Im_WebSession(url_has_any=DomainNames)
| extend IPAddress=IpAddr, DeviceName=Hostname, AccountName = tostring(split(User, "@")[0]), AccountDomain = tostring(split(User, "@")[1])
Applied to the Combined Result
| where DNSName has_any (DomainNames)
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq |
DNSName | match |
| field:"DNSName" kind:match |
DestinationHost | match |
| field:"DestinationHost" kind:match |
DnsQuery | match |
| field:"QueryName" kind:match |
Fqdn | is_not_null | field:"Fqdn" kind:is_not_null | |
Fqdn | match |
| field:"Fqdn" kind:match |
Message | match |
| field:"Message" kind:match |
QueryName | is_not_null | field:"QueryName" kind:is_not_null | |
QueryName | match |
| field:"QueryName" kind:match |
RemoteDnsCanonicalNames | match |
| field:"RemoteDnsCanonicalNames" kind:match |
Request_Name | match |
| field:"Request_Name" kind:match |
ResourceType | eq |
| field:"ResourceType" kind:eq value:"AZUREFIREWALLS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Resource | project |
TimeGenerated | project |
Type | project |
msg_s | project |
DNSName | extend |
IPAddress | extend |
AccountDomain | extend |
AccountName | extend |
DeviceName | extend |
timestamp | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
Match Legitimate Name or Location - 2
#Attackers often match or approximate the name or location of legitimate files to avoid detection rules that are based trust of on certain operating system processes. This query detects mismatches in the parent-child relationship of core operating system processes to uncover different masquerading attempts.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceProcessEvents action any: Process activity |
Rule Body
id: dd22dc4f-ab7c-4d0a-84ad-cc393638ba31
name: Match Legitimate Name or Location - 2
description: |
Attackers often match or approximate the name or location of legitimate files to avoid detection rules that are based trust of on certain operating system processes.
This query detects mismatches in the parent-child relationship of core operating system processes to uncover different masquerading attempts.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1036.005
query: |
let ProcessRelations=datatable(ImageFile:string,ExpectedParent:dynamic) [
"smss.exe", dynamic(["smss.exe", "ntoskrnl.exe", ""]),
"crmss.exe", dynamic(["smss.exe"]),
"wininit.exe", dynamic(["smss.exe"]),
"winlogon.exe", dynamic(["smss.exe"]),
"services.exe", dynamic(["wininit.exe"]),
"lsaiso.exe", dynamic(["wininit.exe"]),
"lsass.exe", dynamic(["wininit.exe"]),
"spoolsv.exe", dynamic(["services.exe"]),
"dllhost.exe", dynamic(["svchost.exe", "services.exe"]),
"lsm.exe", dynamic(["wininit.exe"]),
"svchost.exe", dynamic(["services.exe", "msmpeng.exe"]),
"runtimebroker.exe", dynamic(["svchost.exe"]),
"taskhostw.exe", dynamic(["svchost.exe"]),
"userinit.exe", dynamic(["winlogon.exe"])
// Explorer can have a lot of parents in some environments
//,"explorer.exe", dynamic(["userinit.exe"])
];
DeviceProcessEvents
| extend ImageFile = tostring(tolower(parse_path(tostring(FolderPath)).Filename))
| extend ParentFile = tostring(tolower(parse_path(tostring(InitiatingProcessFolderPath)).Filename))
| lookup kind=inner ProcessRelations on ImageFile
| where not(set_has_element(ExpectedParent,ParentFile))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: Account
fieldMappings:
- identifier: Sid
columnName: AccountSid
- identifier: Name
columnName: AccountName
- identifier: NTDomain
columnName: AccountDomain
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: ProcessCommandLine
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: ProcessRelations
let ProcessRelations = datatable(ImageFile:string,ExpectedParent:dynamic) [
"smss.exe", dynamic(["smss.exe", "ntoskrnl.exe", ""]),
"crmss.exe", dynamic(["smss.exe"]),
"wininit.exe", dynamic(["smss.exe"]),
"winlogon.exe", dynamic(["smss.exe"]),
"services.exe", dynamic(["wininit.exe"]),
"lsaiso.exe", dynamic(["wininit.exe"]),
"lsass.exe", dynamic(["wininit.exe"]),
"spoolsv.exe", dynamic(["services.exe"]),
"dllhost.exe", dynamic(["svchost.exe", "services.exe"]),
"lsm.exe", dynamic(["wininit.exe"]),
"svchost.exe", dynamic(["services.exe", "msmpeng.exe"]),
"runtimebroker.exe", dynamic(["svchost.exe"]),
"taskhostw.exe", dynamic(["svchost.exe"]),
"userinit.exe", dynamic(["winlogon.exe"])
];
Stage 1: source
DeviceProcessEvents
Stage 2: extend
| extend ImageFile = tostring(tolower(parse_path(tostring(FolderPath)).Filename))
Stage 3: extend
| extend ParentFile = tostring(tolower(parse_path(tostring(InitiatingProcessFolderPath)).Filename))
Stage 4: kusto:lookup
lookup kind=inner (ProcessRelations) on ImageFile
Stage 5: where
where not (ParentFile =~ "ExpectedParent")
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ParentFile | eq | ExpectedParent | excludes:ParentFile field:"ParentFile" value:"ExpectedParent" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ImageFile | extend |
ParentFile | extend |
Multiple users email forwarded to same destination
#'Identifies when multiple (more than one) users mailboxes are configured to forward to the same destination. This could be an attacker-controlled destination mailbox configured to collect mail from multiple compromised user accounts.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection | |
| Exfiltration |
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: 871ba14c-88ef-48aa-ad38-810f26760ca3
name: Multiple users email forwarded to same destination
description: |
'Identifies when multiple (more than one) users mailboxes are configured to forward to the same destination.
This could be an attacker-controlled destination mailbox configured to collect mail from multiple compromised user accounts.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Office365
dataTypes:
- OfficeActivity (Exchange)
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
- Exfiltration
relevantTechniques:
- T1114
- T1020
query: |
let queryfrequency = 1d;
let queryperiod = 7d;
OfficeActivity
| where TimeGenerated > ago(queryperiod)
| where OfficeWorkload =~ "Exchange"
//| where Operation in ("Set-Mailbox", "New-InboxRule", "Set-InboxRule")
| where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
| mv-apply DynamicParameters = todynamic(Parameters) on (summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)))
| evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')
| extend DestinationMailAddress = tolower(case(
isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""),
isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""),
isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")),
""))
| where isnotempty(DestinationMailAddress)
| mv-expand split(DestinationMailAddress, ";")
| extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]
| extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP
| where DistinctUserCount > 1 and EndTime > ago(queryfrequency)
| mv-expand UserId to typeof(string)
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserId
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
version: 2.0.3
kind: Scheduled
Stages and Predicates
Parameters
let queryfrequency = 1d;
let queryperiod = 7d;
Stage 1: source
OfficeActivity
Stage 2: where
| where TimeGenerated > ago(queryperiod)
Stage 3: where
| where OfficeWorkload =~ "Exchange"
Stage 4: where
| where Parameters has_any ("ForwardTo", "RedirectTo", "ForwardingSmtpAddress")
Stage 5: kusto:mv-apply
| mv-apply DynamicParameters = todynamic(Parameters) on (summarize ParsedParameters = make_bag(bag_pack(tostring(DynamicParameters.Name), DynamicParameters.Value)))
Stage 6: evaluate
| evaluate bag_unpack(ParsedParameters, columnsConflict='replace_source')
Stage 7: extend
| extend DestinationMailAddress = tolower(case(
isnotempty(column_ifexists("ForwardTo", "")), column_ifexists("ForwardTo", ""),
isnotempty(column_ifexists("RedirectTo", "")), column_ifexists("RedirectTo", ""),
isnotempty(column_ifexists("ForwardingSmtpAddress", "")), trim_start(@"smtp:", column_ifexists("ForwardingSmtpAddress", "")),
""))
Stage 8: where
| where isnotempty(DestinationMailAddress)
Stage 9: mv-expand
| mv-expand split(DestinationMailAddress, ";")
Stage 10: extend
| extend ClientIPValues = extract_all(@'\[?(::ffff:)?(?P<IPAddress>(\d+\.\d+\.\d+\.\d+)|[^\]]+)\]?([-:](?P<Port>\d+))?', dynamic(["IPAddress", "Port"]), ClientIP)[0]
Stage 11: extend
| extend ClientIP = tostring(ClientIPValues[0]), Port = tostring(ClientIPValues[1])
Stage 12: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), DistinctUserCount = dcount(UserId), UserId = make_set(UserId, 250), Ports = make_set(Port, 250), EventCount = count() by tostring(DestinationMailAddress), ClientIP
Stage 13: where
| where DistinctUserCount > 1 and EndTime > ago(queryfrequency)
Stage 14: mv-expand
| mv-expand UserId to typeof(string)
Stage 15: extend
| extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationMailAddress | is_not_null | field:"DestinationMailAddress" kind:is_not_null | |
DistinctUserCount | gt |
| field:"DistinctUserCount" kind:gt value:"1" |
OfficeWorkload | eq |
| field:"m365::Workload" kind:eq value:"Exchange" |
Parameters | match |
| field:"m365::Parameters" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ClientIP | summarize |
DistinctUserCount | summarize |
EndTime | summarize |
EventCount | summarize |
Ports | summarize |
StartTime | summarize |
UserId | summarize |
AccountName | extend |
AccountUPNSuffix | extend |
Pathlock TDnR - SAP Batch Job Events
#Detects security-relevant SAP batch processing events, forwarded by Pathlock Threat Detection and Response. Malicious or unauthorized batch jobs may be used to execute code, exfiltrate data, or establish persistent tasks within the SAP environment.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution | |
| Persistence |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c04
name: Pathlock TDnR - SAP Batch Job Events
kind: Scheduled
description: >-
Detects security-relevant SAP batch processing events, forwarded by Pathlock Threat Detection and Response.
Malicious or unauthorized batch jobs may be used to execute code, exfiltrate data, or
establish persistent tasks within the SAP environment.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
- Persistence
relevantTechniques:
- T1053
- T1059
query: |
Pathlock_TDnR_CL
| where DataSource == "BATCH_JOBS"
| 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 == "BATCH_JOBS"
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:"BATCH_JOBS" |
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 |
Pure Controller Failed
#Detect controller failure and take appropriate response action.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Rule Body
id: c317b007-84e7-4449-93f4-4444f6638fd0
name: Pure Controller Failed
version: 1.0.0
kind: NRT
description: Detect controller failure and take appropriate response action.
severity: High
tactics:
- Execution
relevantTechniques:
- T0871
query: |-
Syslog
| where SyslogMessage has "purity.alert"
| extend Message = replace_regex(SyslogMessage, "#012", "\n")
| extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\]:\s(?P<object>.*)\[(?P<responsecode>\w+)\][\s\S]*Severity:\s*(?P<severity>\S+)\s*(Tag:\s*(?P<reason>\S+))?\s*UTC([\s\S]*)Array Name:\s*(?P<objectname>\S+)\s*Domain:\s*(?P<domainorigin>\S+)\s*(?P<part2log>[\s\S]*))", dynamic(['process','processid','object','objectname','responsecode','severity','reason','domainorigin','part2log']), Message)
| mv-expand ParsedLog
| extend ResidueLog = tostring(ParsedLog[8])
| extend Rlog = extract_all(@"(((Suggested Action:\s*(?P<action>[\s\S]*)\s*Knowledge Base Article:\s*(?P<url>.*))|(Knowledge Base Article:\s*(?P<url>.*)\s*Suggested Action:\s*(?P<action>.*)\s*)|(Suggested Action:\s*(?P<action>[\s\S]*)))(([\s\S]*)Purity Version:\s*(?P<pversion>.*))?\s*([\s\S]*)Variables: \(below\)\s*(?P<subject>[\s\S]*))", dynamic(['action','url','pversion','subject']),ResidueLog)
| mv-expand Rlog
| extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureObject = ParsedLog[2], PureCode = ParsedLog[4], PureSeverity = ParsedLog[5], PureReason = ParsedLog[6], PureObjectName = ParsedLog[3], PureDomainOrigin = ParsedLog[7], PureAction = Rlog[0], PureUrl = Rlog[1], PureVersion = Rlog[2], PureMessage = Rlog[3]
| project-away ResidueLog, Rlog, ParsedLog
| where PureObject matches regex @"(Controllers ct[0-9] have failed)"
suppressionDuration: 5h
suppressionEnabled: false
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDynamicProperties: []
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: HostIP
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
Stages and Predicates
Stage 1: source
Syslog
Stage 2: where
| where SyslogMessage has "purity.alert"
Stage 3: extend
| extend Message = replace_regex(SyslogMessage, "#012", "\n")
Stage 4: extend
| extend ParsedLog = extract_all(@"((?P<process>.*?)\[(?P<processid>.*?)\]:\s(?P<object>.*)\[(?P<responsecode>\w+)\][\s\S]*Severity:\s*(?P<severity>\S+)\s*(Tag:\s*(?P<reason>\S+))?\s*UTC([\s\S]*)Array Name:\s*(?P<objectname>\S+)\s*Domain:\s*(?P<domainorigin>\S+)\s*(?P<part2log>[\s\S]*))", dynamic(['process','processid','object','objectname','responsecode','severity','reason','domainorigin','part2log']), Message)
Stage 5: mv-expand
| mv-expand ParsedLog
Stage 6: extend
| extend ResidueLog = tostring(ParsedLog[8])
Stage 7: extend
| extend Rlog = extract_all(@"(((Suggested Action:\s*(?P<action>[\s\S]*)\s*Knowledge Base Article:\s*(?P<url>.*))|(Knowledge Base Article:\s*(?P<url>.*)\s*Suggested Action:\s*(?P<action>.*)\s*)|(Suggested Action:\s*(?P<action>[\s\S]*)))(([\s\S]*)Purity Version:\s*(?P<pversion>.*))?\s*([\s\S]*)Variables: \(below\)\s*(?P<subject>[\s\S]*))", dynamic(['action','url','pversion','subject']),ResidueLog)
Stage 8: mv-expand
| mv-expand Rlog
Stage 9: extend
| extend PureLogType = ParsedLog[0], PureProcessID = ParsedLog[1], PureObject = ParsedLog[2], PureCode = ParsedLog[4], PureSeverity = ParsedLog[5], PureReason = ParsedLog[6], PureObjectName = ParsedLog[3], PureDomainOrigin = ParsedLog[7], PureAction = Rlog[0], PureUrl = Rlog[1], PureVersion = Rlog[2], PureMessage = Rlog[3]
Stage 10: project-away
| project-away ResidueLog, Rlog, ParsedLog
Stage 11: where
| where PureObject matches regex @"(Controllers ct[0-9] have failed)"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PureObject | regex_match |
| field:"PureObject" kind:regex_match value:"(Controllers ct[0-9] have failed)" |
SyslogMessage | match |
| field:"SyslogMessage" kind:match value:"purity.alert" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Message | extend |
PureAction | extend |
PureCode | extend |
PureDomainOrigin | extend |
PureLogType | extend |
PureMessage | extend |
PureObject | extend |
PureObjectName | extend |
PureProcessID | extend |
PureReason | extend |
PureSeverity | extend |
PureUrl | extend |
PureVersion | extend |
Sentinel One - Alert from custom rule
#'Detects when alert from custom rule received.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 5f37de91-ff2b-45fb-9eda-49e9f76a3942
name: Sentinel One - Alert from custom rule
description: |
'Detects when alert from custom rule received.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SentinelOne
dataTypes:
- SentinelOne
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
query: |
SentinelOne
| where ActivityType == 3608
| extend RuleName = extract(@'Custom Rule:\s(.*?)\sin Group', 1, EventOriginalMessage)
| extend DstHostname = extract(@'detected on\s(\S+)\.', 1, EventOriginalMessage)
| extend HostCustomEntity = DstHostname
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
SentinelOne
Stage 2: where
| where ActivityType == 3608
Stage 3: extend (3 consecutive steps)
| extend RuleName = extract(@'Custom Rule:\s(.*?)\sin Group', 1, EventOriginalMessage)
| extend DstHostname = extract(@'detected on\s(\S+)\.', 1, EventOriginalMessage)
| extend HostCustomEntity = DstHostname
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityType | eq |
| field:"ActivityType" kind:eq value:"3608" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
RuleName | extend |
DstHostname | extend |
HostCustomEntity | extend |