Detection rules › Kusto

Kusto rules: 3e

Alsid LSASS Memory

#
Status
available
Severity
high
Time window
2h
Source
github.com/Azure/Azure-Sentinel

'Searches for OS Credentials dumping attacks'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
Codenameeq
  • OS Credential Dumping: LSASS Memory corpus 3 (kusto 3)
field:"Codename" kind:eq value:"OS Credential Dumping: LSASS Memory"
MessageTypeeq
  • 2 corpus 21 (kusto 21)
field:"MessageType" kind:eq value:"2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

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.

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

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.

FieldKindValuesSearch
Findingeq
  • AzureT0MGGrantRole
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.

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

'Display incidents in which low sensitivity financial information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

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.

Status
available
Severity
low
Time window
30m
Source
github.com/Azure/Azure-Sentinel

'Triggers when a monitored domain is about to expire. Expired domains risk service disruption, takeover, or misuse by adversaries.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
Serviceeq
  • domain_expiry
field:"Service" kind:eq value:"domain_expiry"

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

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.

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

"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

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.

FieldKindExcluded valuesSearch
patterncontainsfile:hashesexcludes:pattern field:"pattern" value:"file:hashes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConfidenceScoreproject
Countryproject
Descriptionproject
Domainproject
IPAbuseproject
IPv4project
IPv6project
IndicatorIDproject
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
URLproject
createdproject
modifiedproject
nameproject
valid_fromproject

Dataverse - Organization settings modified

#
Status
available
Severity
informational
Time window
14d
Group by
FieldName
Source
github.com/Azure/Azure-Sentinel

Identifies changes made at organization level in the Dataverse environment.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
Descriptionproject
DisplayNameproject
FieldNameproject
InstanceUrlproject
TimeGeneratedproject
UPNSuffixproject
UserIdproject
Valueproject

Detect suspicious conditional access policy modifications

#
Group by
UserPrincipalName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

References

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
JustifiedPIMeq
  • false
field:"JustifiedPIM" kind:eq value:"false"
NewValueIsTrustedeq
  • true
field:"NewValueIsTrusted" kind:eq value:"true"
OperationNamecontains
  • completed (PIM activation)
field:"OperationName" kind:contains value:"completed (PIM activation)"
OperationNameeq
  • Add member to group
  • Remove member from group
field:"OperationName" kind:eq
OperationNamein
  • Add named location
  • Delete conditional access policy
  • Update conditional access policy
  • Update named location
field:"OperationName" kind:in
PimJustificationmatch
  • CA transforms: term
  • Conditional Access transforms: term
  • Location transforms: term
  • Named transforms: term
  • Trusted transforms: term
field:"PimJustification" kind:match
Reasonsne
  • []
field:"Reasons" kind:ne value:"[]"
TargetResources_modifiedPropertiescontains
  • CA-Exclude
  • CA-Include
field:"TargetResources_modifiedProperties" kind:contains
TargetResources_modifiedProperties.displayNameeq
  • Group.DisplayName transforms: kusto_dynamic_path
field:"TargetResources_modifiedProperties.displayName" kind:eq value:"Group.DisplayName"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADOperationTypeproject
InitiatedByproject
LoggedByServiceproject
OperationNameproject
Reasonsproject
Resultproject
TargetResourcesproject
TimeGeneratedproject
UserPrincipalNameextend
JustifiedPIMextend

F&O - Unusual sign-in activity using single factor authentication

#
Status
available
Severity
low
Time window
14d
Group by
AppDisplayName, IPAddress, Location, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 used in Stage 5

let historical_sign_in_locations = historical_sign_in_activity
    | summarize by Location;

Let Binding: multifactor_sign_in_count used in Stage 4

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.

FieldKindValuesSearch
AppIdin
  • 00000015-0000-0000-c000-000000000000
  • 913c6de4-2a4a-4a61-a9ce-945d2b2ce2e0
field:"AppId" kind:in
ResultTypeeq
  • 0 corpus 8 (kusto 8)
field:"ResultType" kind:eq value:"0"
multifactor_sign_in_countgt
  • 0 transforms: number
field:"multifactor_sign_in_count" kind:gt value:"0"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Locationeqhistorical_sign_in_locationsexcludes:Location field:"Location" value:"historical_sign_in_locations"
NetworkLocationDetailsmatchtrustedNamedLocationexcludes:NetworkLocationDetails field:"NetworkLocationDetails" value:"trustedNamedLocation"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AppDisplayNameproject
CloudAppIdproject
IPAddressproject
Locationproject
UPNSuffixproject
UserPrincipalNameproject

Filewall - Blocked files

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

Identifies files blocked by Filewall for Microsoft 365 (SharePoint/OneDrive/Teams).

MITRE ATT&CK Coverage

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.

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.

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

'This query searches for events found on code Snippet (paste) sharing platform.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
index_nameextend

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.

Status
available
Severity
high
Time window
14d
Group by
id_s
Source
github.com/Azure/Azure-Sentinel

'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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
id_ssummarize
timestampextend
DNSCustomEntityextend

Known Forest Blizzard group domains - July 2019

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

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

TacticTechniques
Command & Control

Telemetry Coverage

ProviderRecord / event type
SysmonEvent 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.

FieldKindValuesSearch
Categoryeq
  • AzureFirewallApplicationRule corpus 3 (kusto 3)
  • AzureFirewallDnsProxy corpus 2 (kusto 2)
field:"Category" kind:eq
DNSNamematch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"DNSName" kind:match
DestinationHostmatch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"DestinationHost" kind:match
DnsQuerymatch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"QueryName" kind:match
Fqdnis_not_null
  • (no value, null check)
field:"Fqdn" kind:is_not_null
Fqdnmatch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"Fqdn" kind:match
Messagematch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"Message" kind:match
QueryNameis_not_null
  • (no value, null check)
field:"QueryName" kind:is_not_null
QueryNamematch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"QueryName" kind:match
RemoteDnsCanonicalNamesmatch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"RemoteDnsCanonicalNames" kind:match
Request_Namematch
  • accounts-web-mail.com transforms: term
  • com-mailbox.com transforms: term
  • customer-certificate.com transforms: term
  • irf.services transforms: term
  • microsoft-onthehub.com transforms: term
  • msofficelab.com transforms: term
  • my-sharefile.com transforms: term
  • my-sharepoints.com transforms: term
  • onedrive-sharedfile.com transforms: term
  • onedrv-live.com transforms: term
  • session-users-activities.com transforms: term
  • soros-my-sharepoint.com transforms: term
  • support-servics.net transforms: term
  • transparencyinternational-my-sharepoint.com transforms: term
  • transparencyinternational-my-sharepoints.com transforms: term
  • user-profile-credentials.com transforms: term
  • verify-linke.com transforms: term
field:"Request_Name" kind:match
ResourceTypeeq
  • AZUREFIREWALLS corpus 3 (kusto 3)
field:"ResourceType" kind:eq value:"AZUREFIREWALLS"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Resourceproject
TimeGeneratedproject
Typeproject
msg_sproject
DNSNameextend
IPAddressextend
AccountDomainextend
AccountNameextend
DeviceNameextend
timestampextend
DomainIndexextend
HostNameextend
HostNameDomainextend

Match Legitimate Name or Location - 2

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

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

Telemetry Coverage

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 used in Stage 4

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.

FieldKindExcluded valuesSearch
ParentFileeqExpectedParentexcludes:ParentFile field:"ParentFile" value:"ExpectedParent"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ImageFileextend
ParentFileextend

Multiple users email forwarded to same destination

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

'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

TacticTechniques
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.

FieldKindValuesSearch
DestinationMailAddressis_not_null
  • (no value, null check)
field:"DestinationMailAddress" kind:is_not_null
DistinctUserCountgt
  • 1 transforms: number
field:"DistinctUserCount" kind:gt value:"1"
OfficeWorkloadeq
  • Exchange
field:"m365::Workload" kind:eq value:"Exchange"
Parametersmatch
  • ForwardTo transforms: term
  • ForwardingSmtpAddress transforms: term
  • RedirectTo transforms: term
field:"m365::Parameters" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPsummarize
DistinctUserCountsummarize
EndTimesummarize
EventCountsummarize
Portssummarize
StartTimesummarize
UserIdsummarize
AccountNameextend
AccountUPNSuffixextend

Pathlock TDnR - SAP Batch Job Events

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

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

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.

Output Fields

These fields are emitted when the rule matches.

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

Pure Controller Failed

#
Severity
high
Source
github.com/Azure/Azure-Sentinel

Detect controller failure and take appropriate response action.

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Messageextend
PureActionextend
PureCodeextend
PureDomainOriginextend
PureLogTypeextend
PureMessageextend
PureObjectextend
PureObjectNameextend
PureProcessIDextend
PureReasonextend
PureSeverityextend
PureUrlextend
PureVersionextend

Sentinel One - Alert from custom rule

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

'Detects when alert from custom rule received.'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
ActivityTypeeq
  • 3608 corpus 3 (kusto 3)
field:"ActivityType" kind:eq value:"3608"

Output Fields

These fields are emitted when the rule matches.

FieldSource
RuleNameextend
DstHostnameextend
HostCustomEntityextend

Back to all Kusto rules