Detection rules › Kusto

Kusto rules: 39

Cisco SDWAN - Intrusion Events

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

'This Analytic rule will monitor Intrusion events in Cisco syslog data based on the provided Signature ID. This will create an incident if that Signature ID is found in the specified time range.'

MITRE ATT&CK Coverage

Rule Body

id: 232a1c75-63fc-4c81-8b18-b4a739fccba8
name: Cisco SDWAN - Intrusion Events
description: |
  'This Analytic rule will monitor Intrusion events in Cisco syslog data based on the provided Signature ID. This will create an incident if that Signature ID is found in the specified time range.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CiscoSDWAN
    dataTypes:
      - CiscoSyslogUTD
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1189
query: |
  CiscoSyslogUTD
  | where SignatureId == "Enter SignatureId"
  | distinct SignatureId,SourceIP
incidentConfiguration:
  createIncident: true
eventGroupingSettings:
    aggregationKind: AlertPerResult
customDetails:
  signatureid: SignatureId
entityMappings:
  - entityType: RegistryKey
    fieldMappings:
      - identifier: Key
        columnName: SignatureId
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSyslogUTD

Stage 2: where

| where SignatureId == "Enter SignatureId"

Stage 3: distinct

| distinct SignatureId,SourceIP

Indicators

These rows show field, operator, and value matches.

Cloudflare - Multiple user agents for single source

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

'Detects requests with different user agents from one source in short timeframe.'

MITRE ATT&CK Coverage

Rule Body

id: fc50076a-0275-43d5-b9dd-38346c061f68
name: Cloudflare - Multiple user agents for single source
description: |
  'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 10;
  Cloudflare
  | where isnotempty(HttpUserAgentOriginal)
  | summarize d_ua = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 3m)
  | where d_ua > threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

Cloudflare

Stage 2: where

| where isnotempty(HttpUserAgentOriginal)

Stage 3: summarize

| summarize d_ua = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 3m)

Stage 4: where

| where d_ua > threshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
HttpUserAgentOriginalis_not_null
  • (no value, null check)
field:"HttpUserAgentOriginal" kind:is_not_null
d_uagt
  • 10 transforms: number
field:"d_ua" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
d_uasummarize

Cognni Incidents for Highly Sensitive HR 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
high
Time window
5h
Source
github.com/Azure/Azure-Sentinel

'Display incidents in which highly sensitive HR information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: f68846cf-ec99-497d-9ce1-80a9441564fb
name: Cognni Incidents for Highly Sensitive HR Information
description: |
  'Display incidents in which highly sensitive HR information was placed at risk by user sharing.'
severity: High
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let highRisk = 3;
  let hr = 'HR Information';
  CognniIncidents_CL 
  | where Severity == highRisk
  | where informationType_s == hr
  | 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 highRisk = 3;
let hr = 'HR Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == highRisk

Stage 3: where

| where informationType_s == hr

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

CTE - Device Enrolled but On Hold

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

A device enrollment is on hold, typically due to a license/device-capacity limit being reached.

Rule Body

id: 21818920-85eb-4955-a7cc-f62c731f32ac
name: CTE - Device Enrolled but On Hold
description: |
  A device enrollment is on hold, typically due to a license/device-capacity limit being reached.
severity: Low
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  []
relevantTechniques: []
query: |
  CybereinforceCTE_CL
  | where EventType == "device.onhold"
  | extend d = parse_json(Details)
  | extend
      HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
      DeviceId = tostring(d.deviceId),
      DeviceUser = tostring(d.deviceUser),
      DeviceOs = tostring(d.deviceOs),
      AppName = "Cybereinforce"
  | project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, AppName, Actor, Severity, Category, Source
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "device.onhold"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend
    HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
    DeviceId = tostring(d.deviceId),
    DeviceUser = tostring(d.deviceUser),
    DeviceOs = tostring(d.deviceOs),
    AppName = "Cybereinforce"
HostName =
ifisnotempty(tostring(d.hostname))tostring(d.hostname)
elsestrcat("cte-device-", substring(EntityId, 0, 8))

Stage 5: project

| project TimeGenerated, TenantId, DeviceId, HostName, DeviceUser, DeviceOs, AppName, Actor, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actorproject
AppNameproject
Categoryproject
DeviceIdproject
DeviceOsproject
DeviceUserproject
HostNameproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject

CyberArkEPM - Uncommon process Internet access

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

'Detects access to the Internet by uncommon processes.'

MITRE ATT&CK Coverage

Rule Body

id: 9d0d44ab-54dc-472a-9931-53521e888932
name: CyberArkEPM - Uncommon process Internet access
description: |
  'Detects access to the Internet by uncommon processes.'
severity: High
requiredDataConnectors:
  - connectorId: CyberArkEPM
    dataTypes:
      - CyberArkEPM
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - DefenseEvasion
  - CommandAndControl
relevantTechniques:
  - T1204
  - T1036
  - T1095
query: | 
  let lb_period = 14d;
  let q_time = 1h;
  let inet_access_proc = CyberArkEPM
  | where TimeGenerated between (ago(lb_period) .. ago(q_time))
  | where EventSubType =~ 'DetectAccessInternet'
  | where isnotempty(ActingProcessFileInternalName)
  | summarize makeset(ActingProcessFileInternalName);
  CyberArkEPM
  | where TimeGenerated > ago(q_time)
  | where EventSubType =~ 'DetectAccessInternet'
  | where ActingProcessFileInternalName !in (inet_access_proc)
  | extend AccountCustomEntity = ActorUsername
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lb_period = 14d;
let q_time = 1h;

Let Binding: inet_access_proc used in Stage 4

let inet_access_proc = CyberArkEPM
| where TimeGenerated between (ago(lb_period) .. ago(q_time))
| where EventSubType =~ 'DetectAccessInternet'
| where isnotempty(ActingProcessFileInternalName)
| summarize makeset(ActingProcessFileInternalName);

Stage 1: source

CyberArkEPM

Stage 2: where

| where TimeGenerated > ago(q_time)

Stage 3: where

| where EventSubType =~ 'DetectAccessInternet'

Stage 4: where

| where ActingProcessFileInternalName !in (inet_access_proc)

Stage 5: extend

| extend AccountCustomEntity = ActorUsername

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ActingProcessFileInternalNameeqinet_access_procexcludes:ActingProcessFileInternalName field:"ActingProcessFileInternalName" value:"inet_access_proc"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

CYFIRMA - High severity Malicious Network Indicators with Block Action 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
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"This rule detects network-based indicators such as IP addresses, domains, and URLs reported by CYFIRMA threat intelligence with a recommended action of 'Block' and no specified role. These indicators may be associated with infrastructure used in phishing campaigns, malware, or initial access via watering hole or drive-by compromise techniques."

MITRE ATT&CK Coverage

Rule Body

id: 58ae2c87-fc07-434b-aacf-f66d25b25e7e
name: CYFIRMA - High severity Malicious Network Indicators with Block Action Rule
description: |
  "This rule detects network-based indicators such as IP addresses, domains, and URLs reported by CYFIRMA threat intelligence with a recommended action of 'Block' and no specified role. 
  These indicators may be associated with infrastructure used in phishing campaigns, malware, or initial access via watering hole or drive-by compromise techniques."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // Network Indicators with Block Action Rule
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (isempty(Roles) or not(Roles has_any ('Command & Control', 'Malware', 'TOR', 'Trojan', '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
  - Reconnaissance
  - Impact
relevantTechniques:
  - T1566
  - T1189
  - T1204
  - T1595
  - T1498
  - T1566.002
  - T1204.001
  - T1595.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Malicious Network Indicators with Block Action Rule - {{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 TimeGenerated between (ago(timeFrame) .. now())
    and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (isempty(Roles) or not(Roles has_any ('Command & Control', 'Malware', 'TOR', 'Trojan', '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 - Terminated employee exfiltration over email

#
Status
available
Severity
high
Time window
14d
Group by
$left.UserId, $right.SenderAddress, AccountName, UPNSuffix, UserId
Source
github.com/Azure/Azure-Sentinel

This query identifies Dataverse exfiltration via email by terminated employees.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: de039242-47e0-43fa-84d7-b6be24305349
kind: Scheduled
name: Dataverse - Terminated employee exfiltration over email
description: This query identifies Dataverse exfiltration via email by terminated
  employees.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - EmailEvents
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert
  - connectorId: IdentityInfo
    dataTypes:
      - IdentityInfo
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1639
  - T1567
query: |
  // Note this detection relies upon the user's UPN matching their email address.
  // UEBA can provide more accurate data if enabled.
  let query_frequency = 1h;
  let allowed_destination_smtp_domains = dynamic([
  // Specify a list of recipient domains to exclude from alerting.
  // Example:
  // "microsoft.com", "contoso.com"
      ]);
  let exfiltration_alert_users = SecurityAlert
      | where Tactics has 'Exfiltration' and Entities has_all ('account', '32780')
      | mv-expand DataverseEntities = todynamic(Entities)
      | where DataverseEntities.AppId == 32780
      | extend InstanceUrl = tostring(DataverseEntities.InstanceName)
      | mv-expand AccountEntities = todynamic(Entities)
      | where AccountEntities.Type == 'account'
      | extend
          AccountName = tostring(AccountEntities.Name),
          UPNSuffix = tostring(AccountEntities.UPNSuffix)
      | summarize InstanceUrls = make_set(InstanceUrl, 100) by AccountName, UPNSuffix
      | extend UserId = tolower(strcat(AccountName, "@", UPNSuffix));
  exfiltration_alert_users
  | join kind=inner (
      MSBizAppsTerminatedEmployees
      | project UserId = tolower(UserPrincipalName), NotificationDate
      | where startofday(NotificationDate) <= startofday(now()))
      // Uncomment the below KQL if UEBA is available to gain more accurate
      // email address data:
      // | join kind=leftouter (_ASIM_IdentityInfo) on $left.UserId == $right.Username
      // | extend UserId = iif(UserId == UserMailAddress or isempty(UserMailAddress), UserId, UserMailAddress))
      on UserId
  | join kind=inner (
      EmailEvents
      | where TimeGenerated >= ago (query_frequency)
      | where EmailDirection == "Outbound" and AttachmentCount > 0
      | extend RecipientDomain = tolower(split(RecipientEmailAddress, '@')[1])
      | where RecipientDomain !in (allowed_destination_smtp_domains)
      | summarize
          RecipientAddresses = make_set(RecipientEmailAddress, 1000),
          Subject = make_set(Subject, 1000)
          by SenderAddress = tolower(SenderMailFromAddress), SenderIPv4)
      on $left.UserId == $right.SenderAddress
  | mv-expand InstanceUrl = InstanceUrls to typeof(string)
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, "@")[0]),
      UPNSuffix = tostring(split(UserId, "@")[1])
  | project
      UserId,
      InstanceUrl,
      SenderIPv4,
      RecipientAddresses,
      Subject,
      AccountName,
      UPNSuffix,
      CloudAppId
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SenderIPv4
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Email attachment sent externally by terminated user following
    Dataverse exfiltration alerts
  alertDescriptionFormat: 'Departing or terminated user {{UserId}} was found to send
    email to external domains not on the allowed list: {{RecipientAddresses}}'
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 1h;

let exfiltration_alert_users is inlined into the numbered stages below.

Let Binding: allowed_destination_smtp_domains used in Stage 12

let allowed_destination_smtp_domains = dynamic([
    ]);

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

Stage 1: source

SecurityAlert

Stage 2: where

| where Tactics has 'Exfiltration' and Entities has_all ('account', '32780')

Stage 3: mv-expand

| mv-expand DataverseEntities = todynamic(Entities)

Stage 4: where

| where DataverseEntities.AppId == 32780

Stage 5: extend

| extend InstanceUrl = tostring(DataverseEntities.InstanceName)

Stage 6: mv-expand

| mv-expand AccountEntities = todynamic(Entities)

Stage 7: where

| where AccountEntities.Type == 'account'

Stage 8: extend

| extend
        AccountName = tostring(AccountEntities.Name),
        UPNSuffix = tostring(AccountEntities.UPNSuffix)

Stage 9: summarize

| summarize InstanceUrls = make_set(InstanceUrl, 100) by AccountName, UPNSuffix

Stage 10: extend

| extend UserId = tolower(strcat(AccountName, "@", UPNSuffix))

Stage 11: join

exfiltration_alert_users
| join kind=inner (
    MSBizAppsTerminatedEmployees
    | project UserId = tolower(UserPrincipalName), NotificationDate
    | where startofday(NotificationDate) <= startofday(now()))
    on UserId

Stage 12: join

| join kind=inner (
    EmailEvents
    | where TimeGenerated >= ago (query_frequency)
    | where EmailDirection == "Outbound" and AttachmentCount > 0
    | extend RecipientDomain = tolower(split(RecipientEmailAddress, '@')[1])
    | where RecipientDomain !in (allowed_destination_smtp_domains)
    | summarize
        RecipientAddresses = make_set(RecipientEmailAddress, 1000),
        Subject = make_set(Subject, 1000)
        by SenderAddress = tolower(SenderMailFromAddress), SenderIPv4)
    on $left.UserId == $right.SenderAddress

Stage 13: mv-expand

mv-expand InstanceUrl

Stage 14: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 15: project

project AccountName, CloudAppId, InstanceUrl, RecipientAddresses, SenderIPv4, Subject, UPNSuffix, UserId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RecipientDomaineq[]excludes:RecipientDomain field:"RecipientDomain" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
CloudAppIdproject
InstanceUrlproject
RecipientAddressesproject
SenderIPv4project
Subjectproject
UPNSuffixproject
UserIdproject

Discord CDN Risky File Download (ASIM Web Session Schema)

#
Severity
medium
Time window
1d
Group by
DiscordServerId, dcount_Url, max_TimeGenerated, min_TimeGenerated, set_EventResult, set_SrcIpAddr, set_SrcUsername, set_Url
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'

MITRE ATT&CK Coverage

Rule Body

id: 01e8ffff-dc0c-43fe-aa22-d459c4204553
name: Discord CDN Risky File Download  (ASIM Web Session Schema)
description: |
  'Identifies callouts to Discord CDN addresses for risky file extensions. This detection will trigger when a callout for a risky file is made to a discord server that has only been seen once in your environment. 
   Unique discord servers are identified using the server ID that is included in the request URL (DiscordServerId in query). Discord CDN has been used in multiple campaigns to download additional payloads.
   This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM WebSession schema (ASIM WebSession Schema)'
severity: Medium
requiredDataConnectors:
  - connectorId: SquidProxy
    dataTypes:
      - SquidProxy_CL
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071.001
tags:
  - Discord
query: |
  let discord=dynamic(["cdn.discordapp.com", "media.discordapp.com"]);
    _Im_WebSession(url_has_any=discord, eventresult='Success')
    | where Url has "attachments"
    | extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url)
    | summarize dcount(Url), make_set(SrcUsername), make_set(SrcIpAddr), make_set(Url), min(TimeGenerated), max(TimeGenerated), make_set(EventResult) by DiscordServerId
    | mv-expand set_SrcUsername to typeof(string), set_Url to typeof(string), set_EventResult to typeof(string), set_SrcIpAddr to typeof(string)
    | summarize by DiscordServerId, dcount_Url, set_SrcUsername, min_TimeGenerated, max_TimeGenerated, set_EventResult, set_SrcIpAddr, set_Url
    | project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, Result=set_EventResult, SourceUser=set_SrcUsername, SourceIP=set_SrcIpAddr, RequestURL=set_Url
    | where RequestURL has_any (".bin",".exe",".dll",".bin",".msi")
    | extend AccountName = tostring(split(SourceUser, "@")[0]), AccountUPNSuffix = tostring(split(SourceUser, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SourceUser
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
version: 1.1.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Parameters

let discord = dynamic(["cdn.discordapp.com", "media.discordapp.com"]);

Stage 1: source

_Im_WebSession(url_has_any=discord, eventresult='Success')

Stage 2: where

| where Url has "attachments"

Stage 3: extend

| extend DiscordServerId = extract(@"\/attachments\/([0-9]+)\/", 1, Url)

Stage 4: summarize

| summarize dcount(Url), make_set(SrcUsername), make_set(SrcIpAddr), make_set(Url), min(TimeGenerated), max(TimeGenerated), make_set(EventResult) by DiscordServerId

Stage 5: mv-expand

| mv-expand set_SrcUsername to typeof(string), set_Url to typeof(string), set_EventResult to typeof(string), set_SrcIpAddr to typeof(string)

Stage 6: summarize

| summarize by DiscordServerId, dcount_Url, set_SrcUsername, min_TimeGenerated, max_TimeGenerated, set_EventResult, set_SrcIpAddr, set_Url

Stage 7: project

| project StartTime=min_TimeGenerated, EndTime=max_TimeGenerated, Result=set_EventResult, SourceUser=set_SrcUsername, SourceIP=set_SrcIpAddr, RequestURL=set_Url

Stage 8: where

| where RequestURL has_any (".bin",".exe",".dll",".bin",".msi")

Stage 9: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RequestURLmatch
  • .bin transforms: term
  • .dll transforms: term
  • .exe transforms: term
  • .msi transforms: term
field:"RequestURL" kind:match
Urlmatch
  • attachments transforms: term
field:"Url" kind:match value:"attachments"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimeproject
RequestURLproject
Resultproject
SourceIPproject
SourceUserproject
StartTimeproject
AccountNameextend
AccountUPNSuffixextend

External guest invitation followed by Microsoft Entra ID PowerShell signin

#
Status
available
Severity
medium
Time window
1d
Group by
UserPrincipalName, parsedUser
Source
github.com/Azure/Azure-Sentinel

By default guests have capability to invite more external guest users, guests also can do suspicious Microsoft Entra ID enumeration. This detection look at guest users, who have been invited or have invited recently, who also are logging via various PowerShell CLI. Ref : 'https://danielchronlund.com/2021/11/18/scary-azure-ad-tenant-enumeration-using-regular-b2b-guest-accounts/

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: acc4c247-aaf7-494b-b5da-17f18863878a
name: External guest invitation followed by Microsoft Entra ID PowerShell signin
description: |
  'By default guests have capability to invite more external guest users, guests also can do suspicious Microsoft Entra ID enumeration. This detection look at guest users, who have been invited or have invited recently, who also are logging via various PowerShell CLI.
  Ref : 'https://danielchronlund.com/2021/11/18/scary-azure-ad-tenant-enumeration-using-regular-b2b-guest-accounts/'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Persistence
  - Discovery
relevantTechniques:
  - T1078.004
  - T1136.003
  - T1087.004
query: |
  let queryfrequency = 1h;
  let queryperiod = 1d;
  AuditLogs
  | where TimeGenerated > ago(queryperiod)
  | where OperationName in ("Invite external user", "Bulk invite users - started (bulk)", "Invite external user with reset invitation status")
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend InitiatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
  // Uncomment the following line to filter events where the inviting user was a guest user
  //| where InitiatedBy has_any ("live.com#", "#EXT#")
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend InvitedUser = tostring(TargetResource.userPrincipalName)
    )
  | mv-expand UserToCompare = pack_array(InitiatedBy, InvitedUser) to typeof(string)
  | where UserToCompare has_any ("live.com#", "#EXT#")
  | extend
      parsedUser = replace_string(tolower(iff(UserToCompare startswith "live.com#", tostring(split(UserToCompare, "#")[1]), tostring(split(UserToCompare, "#EXT#")[0]))), "@", "_"),
      InvitationTime = TimeGenerated
  | join (
      (union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs)
      | where TimeGenerated > ago(queryfrequency)
      | where UserType != "Member"
      | where AppId has_any                       // This web may contain a list of these apps: https://msshells.net/
          ("1b730954-1685-4b74-9bfd-dac224a7b894",// Azure Active Directory PowerShell
           "04b07795-8ddb-461a-bbee-02f9e1bf7b46",// Microsoft Azure CLI
           "1950a258-227b-4e31-a9cf-717495945fc2",// Microsoft Azure PowerShell
           "a0c73c16-a7e3-4564-9a95-2bdf47383716",// Microsoft Exchange Online Remote PowerShell
           "fb78d390-0c51-40cd-8e17-fdbfab77341b",// Microsoft Exchange REST API Based Powershell
           "d1ddf0e4-d672-4dae-b554-9d5bdfd93547",// Microsoft Intune PowerShell
           "9bc3ab49-b65d-410a-85ad-de819febfddc",// Microsoft SharePoint Online Management Shell
           "12128f48-ec9e-42f0-b203-ea49fb6af367",// MS Teams Powershell Cmdlets
           "23d8f6bd-1eb0-4cc2-a08c-7bf525c67bcd",// Power BI PowerShell
           "31359c7f-bd7e-475c-86db-fdb8c937548e",// PnP Management Shell
           "90f610bf-206d-4950-b61d-37fa6fd1b224",// Aadrm Admin Powershell
           "14d82eec-204b-4c2f-b7e8-296a70dab67e",// Microsoft Graph PowerShell
           "9cee029c-6210-4654-90bb-17e6e9d36617" // Power Platform CLI - pac"
          )
      | summarize arg_min(TimeGenerated, *) by UserPrincipalName
      | extend
          parsedUser = replace_string(UserPrincipalName, "@", "_"),
          SigninTime = TimeGenerated
      )
      on parsedUser
  | project InvitationTime, InitiatedBy, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, OperationName, InvitedUser, SigninTime, SigninCategory = Category1, SigninUserPrincipalName = UserPrincipalName, AppDisplayName, ResourceDisplayName, UserAgent, InvitationAdditionalDetails = AdditionalDetails, InvitationTargetResources = TargetResources
  | extend InvitedUserName = tostring(split(InvitedUser,'@',0)[0]), InvitedUserUPNSuffix = tostring(split(InvitedUser,'@',1)[0]), 
           InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InvitedUser
      - identifier: Name
        columnName: InvitedUserName
      - identifier: UPNSuffix
        columnName: InvitedUserUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatedByName
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let queryfrequency = 1h;
let queryperiod = 1d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(queryperiod)

Stage 3: where

| where OperationName in ("Invite external user", "Bulk invite users - started (bulk)", "Invite external user with reset invitation status")

Stage 4: extend (6 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend InvitedUser = tostring(TargetResource.userPrincipalName)
  )

Stage 6: mv-expand

| mv-expand UserToCompare = pack_array(InitiatedBy, InvitedUser) to typeof(string)

Stage 7: where

| where UserToCompare has_any ("live.com#", "#EXT#")

Stage 8: extend

| extend
    parsedUser = replace_string(tolower(iff(UserToCompare startswith "live.com#", tostring(split(UserToCompare, "#")[1]), tostring(split(UserToCompare, "#EXT#")[0]))), "@", "_"),
    InvitationTime = TimeGenerated

Stage 9: join

join (...) on parsedUser

Stage 10: project

project AppDisplayName, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, InvitationAdditionalDetails, InvitationTargetResources, InvitationTime, InvitedUser, OperationName, ResourceDisplayName, SigninCategory, SigninTime, SigninUserPrincipalName, UserAgent

Stage 11: extend

extend InitiatedByName, InitiatedByUPNSuffix, InvitedUserName, InvitedUserUPNSuffix

Stage 12: summarize aggregation inside the join branch

summarize by UserPrincipalName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AppIdmatch
  • 04b07795-8ddb-461a-bbee-02f9e1bf7b46 transforms: term
  • 12128f48-ec9e-42f0-b203-ea49fb6af367 transforms: term
  • 14d82eec-204b-4c2f-b7e8-296a70dab67e transforms: term
  • 1950a258-227b-4e31-a9cf-717495945fc2 transforms: term
  • 1b730954-1685-4b74-9bfd-dac224a7b894 transforms: term
  • 23d8f6bd-1eb0-4cc2-a08c-7bf525c67bcd transforms: term
  • 31359c7f-bd7e-475c-86db-fdb8c937548e transforms: term
  • 90f610bf-206d-4950-b61d-37fa6fd1b224 transforms: term
  • 9bc3ab49-b65d-410a-85ad-de819febfddc transforms: term
  • 9cee029c-6210-4654-90bb-17e6e9d36617 transforms: term
  • a0c73c16-a7e3-4564-9a95-2bdf47383716 transforms: term
  • d1ddf0e4-d672-4dae-b554-9d5bdfd93547 transforms: term
  • fb78d390-0c51-40cd-8e17-fdbfab77341b transforms: term
field:"AppId" kind:match
OperationNamein
  • Bulk invite users - started (bulk)
  • Invite external user
  • Invite external user with reset invitation status
field:"OperationName" kind:in
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"
UserToComparematch
  • #EXT# transforms: term
  • live.com# transforms: term
field:"UserToCompare" kind:match
UserTypene
  • Member
field:"UserType" kind:ne value:"Member"

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserPrincipalNamesummarize

GitLab - User Impersonation

#
Status
available
Severity
medium
Time window
1d
Group by
$left.TargetDetails, $right.AuthorName
Source
github.com/Azure/Azure-Sentinel

'This queries GitLab Audit Logs for user impersonation. A malicious operator or a compromised admin account could leverage the impersonation feature of GitLab to change code or repository settings bypassing usual processes. This hunting queries allows you to track the audit actions done under impersonation.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 0f4f16a2-b464-4c10-9a42-993da3e15a40
name: GitLab - User Impersonation
description: |
  'This queries GitLab Audit Logs for user impersonation. A malicious operator or a compromised admin account could leverage the impersonation feature of GitLab to change code or repository settings bypassing usual processes. This hunting queries allows you to track the audit actions done under impersonation.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1078
query: |
  let impersonationStart = (GitLabAudit
  | where CustomMessage == 'Started Impersonation');
  let impersonationStop = (GitLabAudit
  | where CustomMessage == 'Stopped Impersonation');
  impersonationStart
  | join kind=inner impersonationStop on $left.TargetID == $right.TargetID and $left.AuthorID == $right.AuthorID 
  | where EventTime1 > EventTime
  | extend TimeGenerated, AuthorID, AuthorName, TargetID, TargetDetails = TargetDetails, IPStart = IPAddress, IPStop = IPAddress1, ImpStartTime = EventTime, ImpStopTime = EventTime1, EntityName
  | join kind=inner (GitLabAudit | extend ActionTime = EventTime, AuthorName) on $left.TargetDetails == $right.AuthorName 
  | where ImpStartTime < ActionTime and ActionTime > ImpStopTime
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPStart
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPStop
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EntityName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetDetails
version: 1.0.2
kind: Scheduled

Stages and Predicates

let impersonationStart is inlined into the numbered stages below.

Let Binding: impersonationStop used in Stage 3

let impersonationStop = (GitLabAudit
| where CustomMessage == 'Stopped Impersonation');

Stages 1 to 2 define let impersonationStart as the rule's main pipeline source. Stages 3 to 7 use it.

Stage 1: source

GitLabAudit

Stage 2: where

| where CustomMessage == 'Started Impersonation'

Stage 3: join

impersonationStart
| join kind=inner impersonationStop on $left.TargetID == $right.TargetID and $left.AuthorID == $right.AuthorID

Stage 4: where

where EventTime1 > EventTime

Stage 5: extend

extend AuthorID, AuthorName, EntityName, IPStart, IPStop, ImpStartTime, ImpStopTime, TargetDetails, TargetID, TimeGenerated

Stage 6: join

| join kind=inner (GitLabAudit | extend ActionTime = EventTime, AuthorName) on $left.TargetDetails == $right.AuthorName

Stage 7: where

where ActionTime > ImpStopTime and ImpStartTime < ActionTime

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CustomMessageeq
  • Started Impersonation
  • Stopped Impersonation
field:"CustomMessage" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AuthorIDextend
AuthorNameextend
EntityNameextend
IPStartextend
IPStopextend
ImpStartTimeextend
ImpStopTimeextend
TargetDetailsextend
TargetIDextend
TimeGeneratedextend

Jamf Protect - Alerts

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

Status
available
Severity
high
Source
github.com/Azure/Azure-Sentinel

'Creates an incident based on Jamf Protect Alert data in Microsoft Sentinel'

Rule Body

id: 6098daa0-f05e-44d5-b5a0-913e63ba3179
name: Jamf Protect - Alerts 
description: |
  'Creates an incident based on Jamf Protect Alert data in Microsoft Sentinel'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: JamfProtect
    dataTypes: 
      - jamfprotectalerts_CL
suppressionDuration: PT5H
suppressionEnabled: false
tactics:
relevantTechniques:
query: |
  jamfprotectalerts_CL
  | extend
      algorithm = "SHA256",
      Host_IPs = tostring(parse_json(DvcIpAddr)[0]),
      Tags = parse_json(tostring(parse_json(tostring(input.match)).tags)),
      Tactics = case(input.match.tags has "Execution", "Execution", input.match.tags has "Visibility", "Visibility", input.match.tags has "Persistence", "Persistence", input.match.tags has "LateralMovement", "LateralMovement", input.match.tags has "CredentialAccess", "CredentialAcccess", input.match.tags has "DefenseEvasion", "DefenseEvasion", input.match.tags has "PrivilegeEscalation", "PrivilegeEscalation", input.match.tags has "Impact", "Impact", input.match.tags has "CommandAndControl", "CommandandControl", input.match.tags has "Discovery", "Discovery", input.match.tags has "InitialAccess", "InitialAccess", ""),
      Techniques = pack_array(extract(@"[A-Za-z]\d{4}", 0, tostring(input.match.tags))),
      JamfPro = case(input.match.actions has "SmartGroup", "Workflow with Jamf Pro", input.match.actions has "Prevented", "No workflow, Prevented by Protect", "No workflow")
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "{{EventMessage}} detected on {{DvcHostname}}"
  alertDescriptionFormat: "{{EventResultMessage}} - Please investigate"
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: EventSeverity
  alertDynamicProperties:
    - alertProperty: ProviderName
      value: EventVendor
    - alertProperty: ProductName
      value: EventProduct
    - alertProperty: Techniques
      value: Techniques
customDetails:
  Protect_Event_Type: EventType
  Protect_Analytic: EventMessage
  Related_File_hash: TargetBinarySHA256
  Related_Binaries: TargetBinaryFilePath
  TargetBinarySignMsg: TargetBinarySigningInfoMessage
  TargetbinarySign: TargetbinarySignerType
  TargetBinarySigner: TargetBinarySigningTeamID
  JamfPro_Status: JamfPro
  Protect_Tags: Tags
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DvcHostname
      - identifier: OSFamily
        columnName: DvcOs
      - identifier: OSVersion
        columnName: DvcOsVersion
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Host_IPs
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: TargetProcessCurrentDirectory
      - identifier: ProcessId
        columnName: TargetProcessId
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: algorithm
      - identifier: Value
        columnName: TargetBinarySHA256
version: 1.0.6
kind: NRT

Stages and Predicates

Stage 1: source

jamfprotectalerts_CL

Stage 2: extend

| extend
    algorithm = "SHA256",
    Host_IPs = tostring(parse_json(DvcIpAddr)[0]),
    Tags = parse_json(tostring(parse_json(tostring(input.match)).tags)),
    Tactics = case(input.match.tags has "Execution", "Execution", input.match.tags has "Visibility", "Visibility", input.match.tags has "Persistence", "Persistence", input.match.tags has "LateralMovement", "LateralMovement", input.match.tags has "CredentialAccess", "CredentialAcccess", input.match.tags has "DefenseEvasion", "DefenseEvasion", input.match.tags has "PrivilegeEscalation", "PrivilegeEscalation", input.match.tags has "Impact", "Impact", input.match.tags has "CommandAndControl", "CommandandControl", input.match.tags has "Discovery", "Discovery", input.match.tags has "InitialAccess", "InitialAccess", ""),
    Techniques = pack_array(extract(@"[A-Za-z]\d{4}", 0, tostring(input.match.tags))),
    JamfPro = case(input.match.actions has "SmartGroup", "Workflow with Jamf Pro", input.match.actions has "Prevented", "No workflow, Prevented by Protect", "No workflow")
JamfPro =
ifinput.match.actions has "SmartGroup""Workflow with Jamf Pro"
elifinput.match.actions has "Prevented""No workflow, Prevented by Protect"
else"No workflow"
Tactics =
ifinput.match.tags has "Execution""Execution"
elifinput.match.tags has "Visibility""Visibility"
elifinput.match.tags has "Persistence""Persistence"
elifinput.match.tags has "LateralMovement""LateralMovement"
elifinput.match.tags has "CredentialAccess""CredentialAcccess"
elifinput.match.tags has "DefenseEvasion""DefenseEvasion"
elifinput.match.tags has "PrivilegeEscalation""PrivilegeEscalation"
elifinput.match.tags has "Impact""Impact"
elifinput.match.tags has "CommandAndControl""CommandandControl"
elifinput.match.tags has "Discovery""Discovery"
elifinput.match.tags has "InitialAccess""InitialAccess"
else""

Output Fields

These fields are emitted when the rule matches.

FieldSource
Host_IPsextend
JamfProextend
Tacticsextend
Tagsextend
Techniquesextend
algorithmextend

NGINX - Sql injection patterns

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

'Detects possible sql injection patterns'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 3bac451d-f919-4c92-9be7-694990e0ca4b
name: NGINX - Sql injection patterns
description: |
  'Detects possible sql injection patterns'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - NGINX_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let sql_patterns = dynamic([@"1/*'*/", @"1'||'asd'||'", @"'1'='1", @"1' or '1'='1", @"1 or 1=1", @"1=1", @"1/*!1111'*/", @"'or''='"]);
  NGINXHTTPServer
  | where UrlOriginal has_any (sql_patterns)
  | extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: sql_patterns used in Stage 2

let sql_patterns = dynamic([@"1/*'*/", @"1'||'asd'||'", @"'1'='1", @"1' or '1'='1", @"1 or 1=1", @"1=1", @"1/*!1111'*/", @"'or''='"]);

Stage 1: source

NGINXHTTPServer

Stage 2: where

| where UrlOriginal has_any (sql_patterns)

Stage 3: extend

| extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
UrlOriginalmatch
  • '1'='1 transforms: term
  • 'or''=' transforms: term
  • 1 or 1=1 transforms: term
  • 1' or '1'='1 transforms: term
  • 1'||'asd'||' transforms: term
  • 1/*!1111'*/ transforms: term
  • 1/*'*/ transforms: term
  • 1=1 transforms: term
field:"UrlOriginal" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend
UrlCustomEntityextend

Pathlock TDnR - Critical File Integrity Changes

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

Detects changes to checksums of critical SAP files, forwarded by Pathlock Threat Detection and Response. File integrity violations may indicate malware installation, unauthorized patching, or tampering with SAP executables and configuration files.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c24
name: Pathlock TDnR - Critical File Integrity Changes
kind: Scheduled
description: >-
  Detects changes to checksums of critical SAP files, forwarded by Pathlock Threat Detection
  and Response. File integrity violations may indicate malware installation, unauthorized
  patching, or tampering with SAP executables and configuration files.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Persistence
relevantTechniques:
  - T1562
  - T1036
query: |
  Pathlock_TDnR_CL
  | where DataSource == "FILE_CHECKSUM"
  | 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 == "FILE_CHECKSUM"

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

Pathlock TDnR - Function Module Tested in Production

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

Detects execution of SAP function modules in a test environment context within production systems, forwarded by Pathlock Threat Detection and Response. This activity may indicate unauthorized code execution, exploitation of function module interfaces, or abuse of debugging capabilities.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c25
name: Pathlock TDnR - Function Module Tested in Production
kind: Scheduled
description: >-
  Detects execution of SAP function modules in a test environment context within production systems,
  forwarded by Pathlock Threat Detection and Response. This activity may indicate unauthorized
  code execution, exploitation of function module interfaces, or abuse of debugging capabilities.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  Pathlock_TDnR_CL
  | where DataSource == "FUNCTION_MODULE_TEST"
  | 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 == "FUNCTION_MODULE_TEST"

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

SUNBURST suspicious SolarWinds child processes

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

Identifies suspicious child processes of SolarWinds.Orion.Core.BusinessLayer.dll that may be evidence of the SUNBURST backdoor References: - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f

MITRE ATT&CK Coverage

TacticTechniques
ExecutionNo specific technique
PersistenceNo specific technique

Telemetry Coverage

Rule Body

id: 4a3073ac-7383-48a9-90a8-eb6716183a54
name: SUNBURST suspicious SolarWinds child processes
description: |
  Identifies suspicious child processes of SolarWinds.Orion.Core.BusinessLayer.dll that may be evidence of the SUNBURST backdoor
  References:
  - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html
  - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Persistence
tags:
  - Solorigate
  - NOBELIUM
query:  |
  let excludeProcs = dynamic([@"\SolarWinds\Orion\APM\APMServiceControl.exe", @"\SolarWinds\Orion\ExportToPDFCmd.Exe", @"\SolarWinds.Credentials\SolarWinds.Credentials.Orion.WebApi.exe", @"\SolarWinds\Orion\Topology\SolarWinds.Orion.Topology.Calculator.exe", @"\SolarWinds\Orion\Database-Maint.exe", @"\SolarWinds.Orion.ApiPoller.Service\SolarWinds.Orion.ApiPoller.Service.exe", @"\Windows\SysWOW64\WerFault.exe"]);
  DeviceProcessEvents
  | where InitiatingProcessFileName =~ "solarwinds.businesslayerhost.exe"
  | where not(FolderPath has_any (excludeProcs))
  | extend
      timestamp = TimeGenerated,
      InitiatingProcessAccountUPNSuffix = tostring(split(InitiatingProcessAccountUpn, "@")[1]),
      Algorithm = "MD5"
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: NTDomain
        columnName: InitiatingProcessAccountDomain
      - identifier: Sid
        columnName: InitiatingProcessAccountSid
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algorithm
      - identifier: Value
        columnName: MD5
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: pemontto
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Let Binding: excludeProcs used in Stage 3

let excludeProcs = dynamic([@"\SolarWinds\Orion\APM\APMServiceControl.exe", @"\SolarWinds\Orion\ExportToPDFCmd.Exe", @"\SolarWinds.Credentials\SolarWinds.Credentials.Orion.WebApi.exe", @"\SolarWinds\Orion\Topology\SolarWinds.Orion.Topology.Calculator.exe", @"\SolarWinds\Orion\Database-Maint.exe", @"\SolarWinds.Orion.ApiPoller.Service\SolarWinds.Orion.ApiPoller.Service.exe", @"\Windows\SysWOW64\WerFault.exe"]);

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where InitiatingProcessFileName =~ "solarwinds.businesslayerhost.exe"

Stage 3: where

| where not(FolderPath has_any (excludeProcs))

Stage 4: extend

| extend
    timestamp = TimeGenerated,
    InitiatingProcessAccountUPNSuffix = tostring(split(InitiatingProcessAccountUpn, "@")[1]),
    Algorithm = "MD5"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
InitiatingProcessFileNameeq
  • solarwinds.businesslayerhost.exe corpus 2 (elastic 1, kusto 1)
field:"parent_process_basename" kind:eq value:"solarwinds.businesslayerhost.exe"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
FolderPathmatch\SolarWinds\Orion\APM\APMServiceControl.exe, \SolarWinds\Orion\ExportToPDFCmd.Exe, \SolarWinds.Credentials\SolarWinds.Credentials.Orion.WebApi.exe, \SolarWinds\Orion\Topology\SolarWinds.Orion.Topology.Calculator.exe, \SolarWinds\Orion\Database-Maint.exe, \SolarWinds.Orion.ApiPoller.Service\SolarWinds.Orion.ApiPoller.Service.exe, \Windows\SysWOW64\WerFault.exeexcludes:FolderPath

Output Fields

These fields are emitted when the rule matches.

FieldSource
Algorithmextend
InitiatingProcessAccountUPNSuffixextend
timestampextend

TI Map Email entity to AzureActivity

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

'Identifies a match in AzureActivity table from any Email IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: a9a4d1ee-0f52-4a1f-8def-a2fb4462104c
name: TI Map Email entity to AzureActivity
description: |
  'Identifies a match in AzureActivity table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  ThreatIntelIndicators
  //Filtering the table for Email related IOCs
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "email-addr"
  | extend EmailSenderAddress = ObservableValue
  | extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | project-reorder *, IsActive, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (
      AzureActivity | where TimeGenerated >= ago(dt_lookBack) and isnotempty(Caller)
      | extend Caller = tolower(Caller)
      | where Caller matches regex emailregex
      | extend AzureActivity_TimeGenerated = TimeGenerated
  )
  on $left.EmailSenderAddress == $right.Caller
  | where AzureActivity_TimeGenerated < ValidUntil
  | summarize AzureActivity_TimeGenerated = arg_max(AzureActivity_TimeGenerated, *) by Id, Caller
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project AzureActivity_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, EmailSenderAddress, EmailSourceDomain, Caller, CallerIpAddress, Url
  | extend Name = tostring(split(Caller, '@', 0)[0]), UPNSuffix = tostring(split(Caller, '@', 1)[0])
  | extend timestamp = AzureActivity_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.12
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "email-addr"

Stage 4: extend (4 consecutive steps)

| extend EmailSenderAddress = ObservableValue
| extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: summarize

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

Stage 7: where

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

Stage 8: project-reorder

| project-reorder *, IsActive, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type

Stage 9: join

| join kind=innerunique (
    AzureActivity | where TimeGenerated >= ago(dt_lookBack) and isnotempty(Caller)
    | extend Caller = tolower(Caller)
    | where Caller matches regex emailregex
    | extend AzureActivity_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.Caller

Stage 10: where

where AzureActivity_TimeGenerated < ValidUntil

Stage 11: summarize

summarize AzureActivity_TimeGenerated by Id, Caller

Stage 12: extend

extend Description

Stage 13: extend

extend ActivityGroupNames

Stage 14: project

project ActivityGroupNames, AzureActivity_TimeGenerated, Caller, CallerIpAddress, Confidence, Description, EmailSenderAddress, EmailSourceDomain, Id, Url, ValidUntil

Stage 15: extend

extend Name, UPNSuffix

Stage 16: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AzureActivity_TimeGeneratedproject
Callerproject
CallerIpAddressproject
Confidenceproject
Descriptionproject
EmailSenderAddressproject
EmailSourceDomainproject
Idproject
Urlproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

Back to all Kusto rules