Detection rules › Kusto

Kusto rules: 1z

AWSCloudTrail - Creation of Glue policy and then privilege escalation

#
Status
available
Severity
medium
Time window
1d
Group by
AccountName, AccountUPNSuffix, EventName, EventSource, PolicyName, RecipientAccountId, SourceIpAddress, UserIdentityArn, UserIdentityType
Source
github.com/Azure/Azure-Sentinel

Identifies creation of IAM policies that grant broad AWS Glue permissions paired with IAM permissions, followed by policy attachment to a principal. This sequence can indicate unauthorized privilege expansion and should be reviewed for potential cloud account manipulation.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 56626956-304f-4408-8ea6-7ba5746ce09e
name: AWSCloudTrail - Creation of Glue policy and then privilege escalation
description: |
  Identifies creation of IAM policies that grant broad AWS Glue permissions paired with IAM
  permissions, followed by policy attachment to a principal. This sequence can indicate unauthorized privilege
  expansion and should be reviewed for potential cloud account manipulation.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1484
  - T1098.003
query: |
  let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
  let createPolicy =  dynamic(["CreatePolicy", "CreatePolicyVersion"]);
  let timeframe = 1d;
  let lookback = 14d;
  // Creating Master table with all the events to use with materialize for better performance
  let EventInfo = AWSCloudTrail
  | where TimeGenerated >= ago(lookback)
  | where EventName in (EventNameList) or EventName in (createPolicy)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "");
  //Checking for Policy creation event with Full Admin Privileges since lookback period.
  let FullAdminPolicyEvents =  materialize(  EventInfo
  | where TimeGenerated >= ago(lookback)
  | where EventName in (createPolicy)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:CreateDevEndpoint" and Action contains "glue:GetDevEndpoints") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:Create*" and Action contains "glue:Get*")) or (Action contains "glue:*") or (Action contains "glue:GetDevEndpoints" and Action contains "glue:UpdateDevEndpoint") or (Action contains "glue:Get*" and Action contains "glue:Update*")) and Resource == "*" and Condition == ""
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, RecipientAccountId, AccountName, AccountUPNSuffix
  | project-rename StartTime = TimeGenerated  );
  let PolicyAttach = materialize(  EventInfo
  | where TimeGenerated >= ago(timeframe)
  | where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
  | summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
  | extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType,"SourceIpAddress", SourceIpAddress, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
  | project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
  );
  // Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
  // These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
  FullAdminPolicyEvents
  | join kind=leftouter
  (
      PolicyAttach
  )
  on PolicyName
  | project-away PolicyName1
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  RecipientAccountId: RecipientAccountId
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Glue privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;

let EventInfo and let FullAdminPolicyEvents are inlined into the numbered stages below.

Let Binding: PolicyAttach used in Stage 15

let PolicyAttach = materialize(  EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType,"SourceIpAddress", SourceIpAddress, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
| project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
);

Stages 1 to 14 define let FullAdminPolicyEvents as the rule's main pipeline source. Stages 15 to 17 use it.

Stage 1: source

AWSCloudTrail

Stage 2: where

| where TimeGenerated >= ago(lookback)

Stage 3: where

| where EventName in (EventNameList) or EventName in (createPolicy)

Stage 4: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 5: where

| where TimeGenerated >= ago(lookback)

Stage 6: where

| where EventName in (createPolicy)

Stage 7: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 8: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 9: mv-expand

| mvexpand Statement

Stage 10: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)

Stage 11: extend

| extend Action = tostring(Action)

Stage 12: where

| where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:CreateDevEndpoint" and Action contains "glue:GetDevEndpoints") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:Create*" and Action contains "glue:Get*")) or (Action contains "glue:*") or (Action contains "glue:GetDevEndpoints" and Action contains "glue:UpdateDevEndpoint") or (Action contains "glue:Get*" and Action contains "glue:Update*")) and Resource == "*" and Condition == ""

Stage 13: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, RecipientAccountId, AccountName, AccountUPNSuffix

Stage 14: project-rename

| project-rename StartTime = TimeGenerated

Stage 15: join

FullAdminPolicyEvents
| join kind=leftouter
(
    PolicyAttach
)
on PolicyName

Stage 16: project-away

project-away PolicyName1

Stage 17: summarize aggregation inside the join branch

summarize by EventSource, EventName, UserIdentityType, UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • glue:*
  • glue:Create*
  • glue:CreateDevEndpoint
  • glue:Get*
  • glue:GetDevEndpoints
  • glue:Update*
  • glue:UpdateDevEndpoint
  • iam:*
  • iam:PassRole
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"ErrorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"ErrorMessage" kind:is_null
EventNamein
  • AttachGroupPolicy corpus 17 (kusto 15, panther 2)
  • AttachGroupPolicy
  • AttachRolePolicy corpus 17 (kusto 15, panther 2)
  • AttachRolePolicy
  • AttachUserPolicy corpus 17 (kusto 15, panther 2)
  • AttachUserPolicy
  • CreatePolicy corpus 14 (kusto 12, panther 2)
  • CreatePolicy
  • CreatePolicyVersion corpus 14 (kusto 12, panther 2)
  • CreatePolicyVersion
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNamesummarize
AccountUPNSuffixsummarize
EventNamesummarize
EventSourcesummarize
PolicyNamesummarize
RecipientAccountIdsummarize
SourceIpAddresssummarize
UserIdentityArnsummarize
UserIdentityTypesummarize

Cisco SE - Generic IOC

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

'This rule is triggered when generic IOC is observed on host.'

MITRE ATT&CK Coverage

Rule Body

id: bccdbc39-31d3-4e2b-9df2-e4c9eecba825
name: Cisco SE - Generic IOC
description: |
  'This rule is triggered when generic IOC is observed on host.'
severity: High
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
relevantTechniques:
  - T1204.002
query: |
  CiscoSecureEndpoint
  | where EventMessage has 'Generic IOC'
  | extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSecureEndpoint

Stage 2: where

| where EventMessage has 'Generic IOC'

Stage 3: extend

| extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventMessagematch
  • Generic IOC transforms: term
field:"EventMessage" kind:match value:"Generic IOC"

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
MalwareCustomEntityextend

CYFIRMA - High severity Command & Control Network Indicators with Monitor Recommendation 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 analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."

MITRE ATT&CK Coverage

Rule Body

id: 6d8fb3fe-2501-4103-8137-34261fa3a596
name: CYFIRMA - High severity Command & Control Network Indicators with Monitor Recommendation Rule
description: |
  "This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
  These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // Network Indicators query with Monitor Recommended Action And C2 Roles
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'c2' or Roles contains 'Command & Control')
  | 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
startTimeUtc: null
tactics:
  - CommandAndControl
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1071
  - T1090
  - T1573
  - T1105
  - T1568
  - T1566
  - T1041
  - T1071.001
  - T1573.001
  - T1568.002
  - T1566.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Command & Control Network Indicators with Monitor Recommendation 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 'Monitor' and (Roles contains 'c2' or Roles contains 'Command & Control')

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

Detect DNS queries reporting multiple errors from different clients - Anomaly Based (ASIM DNS Solution)

#
Status
available
Severity
medium
Time window
14d
Group by
DnsQuery, exists, sumexist
Source
github.com/Azure/Azure-Sentinel

This rule makes use of the series decompose anomaly method to generate an alert when multiple clients report errors for the same DNS query. This rule monitors DNS traffic over a period of 14 days to detect possible similar C2 communication originating from different clients. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK Coverage

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: cf687598-5a2c-46f8-81c8-06b15ed489b1
name: Detect DNS queries reporting multiple errors from different clients - Anomaly Based (ASIM DNS Solution)
description: |
  'This rule makes use of the series decompose anomaly method to generate an alert when multiple clients report errors for the same DNS query. This rule monitors DNS traffic over a period of 14 days to detect possible similar C2 communication originating from different clients. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1573
  - T1008
query: |
  let threshold = 2.5;
  let min_t = ago(14d);
  let max_t = now();
  let dt = 1d;
  let Errors = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
  // calculate avg. eps(events per second)
  let eps = materialize (_Im_Dns | project TimeGenerated | where TimeGenerated > ago(5m) | count | extend Count = Count/300);
  let maxSummarizedTime = toscalar (
    union isfuzzy=true 
        (
            DNS_Summarized_Logs_ip_CL 
                | where EventTime_t >= min_t
                | summarize max_TimeGenerated=max(EventTime_t)
                | extend max_TimeGenerated = datetime_add('hour',1,max_TimeGenerated)
        ),
        (
            print(min_t)
            | project max_TimeGenerated = print_0
        )
      | summarize maxTimeGenerated = max(max_TimeGenerated) 
    );
    let summarizationexist = materialize(
              union isfuzzy=true 
                (
                    DNS_Summarized_Logs_ip_CL
                    | where EventTime_t > ago(1d) 
                    | project v = int(2)
                ),
                (
                    print int(1) 
                    | project v = print_0
                )
                | summarize maxv = max(v)
                | extend sumexist = (maxv > 1)
              );
  let allData = union isfuzzy=true
  (
    (datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) > 1000 | join (summarizationexist) on sumexist) | join (
    _Im_Dns(starttime=todatetime(ago(2d)), endtime=now())
    | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
    | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
    | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
    ) on exists
    | project-away exists, maxv, sum*
  ),
  (
    (datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) between (501 .. 1000) | join (summarizationexist) on sumexist) | join (
    _Im_Dns(starttime=todatetime(ago(3d)), endtime=now())
    | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
    | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
    | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
    ) on exists
    | project-away exists, maxv, sum*
  ),
  (
    (datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) <= 500 | join (summarizationexist) on sumexist) | join (
    _Im_Dns(starttime=todatetime(ago(4d)), endtime=now())
    | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
    | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
    | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
    ) on exists
    | project-away exists, maxv, sum*
  ),
  (
        DNS_Summarized_Logs_ip_CL
        | where EventTime_t > min_t and EventResultDetails_s in (Errors)
        | project-rename SrcIpAddr=SrcIpAddr_s, DnsQuery=DnsQuery_s, Count=count__d, EventTime=EventTime_t
        | extend Count = toint(Count) 
  );
  allData
  | make-series TotalIPCount=dcount(SrcIpAddr) on EventTime from min_t to max_t step dt by DnsQuery
  | extend (anomalies, score, baseline) = series_decompose_anomalies(TotalIPCount, threshold, -1, 'linefit')
  | mv-expand anomalies, score, baseline, EventTime, TotalIPCount
  | extend
    anomalies = toint(anomalies),
    score = toint(score),
    baseline = toint(baseline),
    EventTime = todatetime(EventTime),
    TotalIPs = tolong(TotalIPCount)
  | where EventTime >= ago(dt)
  | where score >= threshold * 2
  | join kind=inner(allData | where TimeGenerated>ago(dt) | summarize SrcIps = make_set(SrcIpAddr,1000) by DnsQuery) on DnsQuery
  | project-away DnsQuery1
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DnsQuery
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  SrcIps: SrcIps
  AnomalyScore: score
  baseline: baseline
  TotalIPs: TotalIPs
alertDetailsOverride:
  alertDisplayNameFormat: "[Anomaly] Multiple errors for the same DNS query has been detected - '{{DnsQuery}}'"
  alertDescriptionFormat: "Multiple errors were detected on different clients for the same DNS query. These unsuccessful responses can be an indication of C2 communication. \n\nBaseline for total clients reporting errors for this DNS query: '{{baseline}}'\n\nCurrent count of clients reporting errors for this DNS query: '{{TotalIPs}}'\n\nClients requesting this DNS query include:\n'{{SrcIps}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 2.5;
let min_t = ago(14d);
let max_t = now();
let dt = 1d;
let Errors = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);

Let Binding: eps

let eps = materialize (_Im_Dns | project TimeGenerated | where TimeGenerated > ago(5m) | count | extend Count = Count/300);

Let Binding: maxSummarizedTime

let maxSummarizedTime = toscalar (
  union isfuzzy=true 
      (
          DNS_Summarized_Logs_ip_CL 
              | where EventTime_t >= min_t
              | summarize max_TimeGenerated=max(EventTime_t)
              | extend max_TimeGenerated = datetime_add('hour',1,max_TimeGenerated)
      ),
      (
          print(min_t)
          | project max_TimeGenerated = print_0
      )
    | summarize maxTimeGenerated = max(max_TimeGenerated) 
  );

Let Binding: summarizationexist

let summarizationexist = materialize(
            union isfuzzy=true 
              (
                  DNS_Summarized_Logs_ip_CL
                  | where EventTime_t > ago(1d) 
                  | project v = int(2)
              ),
              (
                  print int(1) 
                  | project v = print_0
              )
              | summarize maxv = max(v)
              | extend sumexist = (maxv > 1)
            );

Union isfuzzy=true (4 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: datatable(exists:int,, datatable(exists:int,, datatable(exists:int,, DNS_Summarized_Logs_ip_CL

Leg 1: datatable(exists:int,

(datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) > 1000 | join (summarizationexist) on sumexist) | join (
  _Im_Dns(starttime=todatetime(ago(2d)), endtime=now())
  | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
  | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
  | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
  ) on exists
  | project-away exists, maxv, sum*

Leg 2: datatable(exists:int,

(datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) between (501 .. 1000) | join (summarizationexist) on sumexist) | join (
  _Im_Dns(starttime=todatetime(ago(3d)), endtime=now())
  | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
  | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
  | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
  ) on exists
  | project-away exists, maxv, sum*

Leg 3: datatable(exists:int,

(datatable(exists:int, sumexist:bool)[1,false] | where toscalar(eps) <= 500 | join (summarizationexist) on sumexist) | join (
  _Im_Dns(starttime=todatetime(ago(4d)), endtime=now())
  | where TimeGenerated > maxSummarizedTime and EventResultDetails in (Errors)
  | summarize Count=count() by SrcIpAddr, DnsQuery, bin(TimeGenerated,1h)
  | extend EventTime = TimeGenerated, Count = toint(Count), exists=int(1)
  ) on exists
  | project-away exists, maxv, sum*

Leg 4: DNS_Summarized_Logs_ip_CL

DNS_Summarized_Logs_ip_CL
      | where EventTime_t > min_t and EventResultDetails_s in (Errors)
      | project-rename SrcIpAddr=SrcIpAddr_s, DnsQuery=DnsQuery_s, Count=count__d, EventTime=EventTime_t
      | extend Count = toint(Count)

Applied to the Combined Result

| make-series TotalIPCount=dcount(SrcIpAddr) on EventTime from min_t to max_t step dt by DnsQuery | extend (anomalies, score, baseline) = series_decompose_anomalies(TotalIPCount, threshold, -1, 'linefit') | mv-expand anomalies, score, baseline, EventTime, TotalIPCount | extend
  anomalies = toint(anomalies),
  score = toint(score),
  baseline = toint(baseline),
  EventTime = todatetime(EventTime),
  TotalIPs = tolong(TotalIPCount) | where EventTime >= ago(dt) | where score >= threshold * 2 | join kind=inner(allData | where TimeGenerated>ago(dt) | summarize SrcIps = make_set(SrcIpAddr,1000) by DnsQuery) on DnsQuery | project-away DnsQuery1

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsQuerysummarize
TotalIPCountsummarize
anomaliesextend
baselineextend
scoreextend
EventTimeextend
TotalIPsextend

Detect excessive NXDOMAIN DNS queries - Static threshold based (ASIM DNS Solution)

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

This rule generates an alert when the configured threshold for DNS queries to non-existent domains is breached. This helps in identifying possible C2 communications. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 4ab8b09e-3c23-4974-afbe-7e653779eb2b
name: Detect excessive NXDOMAIN DNS queries - Static threshold based (ASIM DNS Solution)
description: |
  'This rule generates an alert when the configured threshold for DNS queries to non-existent domains is breached. This helps in identifying possible C2 communications. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
  - T1008
query: |
  let lookback = 1h;
  let threshold = 200;
  _Im_Dns(starttime=ago(lookback), responsecodename='NXDOMAIN')
  | summarize NXDOMAINCount=count() by SrcIpAddr, bin(TimeGenerated, 15m)
  | where NXDOMAINCount > threshold
  | join kind=inner (_Im_Dns(starttime=ago(lookback), responsecodename='NXDOMAIN')
    | summarize DNSQueries = makeset(DnsQuery) by SrcIpAddr)
    on SrcIpAddr
  | extend NXDOMAINthreshold=threshold
  | project-away SrcIpAddr1
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  DNSQueries: DNSQueries
  NXDOMAINthreshold: NXDOMAINthreshold
  NXDOMAINCount: NXDOMAINCount
alertDetailsOverride:
  alertDisplayNameFormat: "[Static threshold] Excessive NXDOMAIN DNS Queries has been detected from client IP: '{{SrcIpAddr}}'"
  alertDescriptionFormat: "Client is generating excessive amount of DNS queries for non-existent domains. This can be an indication of possible C2 communications. \n\n'NXDOMAIN' error count threshold: '{{NXDOMAINthreshold}}'\n\nCurrent 'NXDOMAIN' error count from this client: '{{NXDOMAINCount}}'\n\nDNS queries requested by the client include:\n\n'{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 1h;
let threshold = 200;

Stage 1: source

_Im_Dns(starttime=ago(lookback), responsecodename='NXDOMAIN')

Stage 2: summarize

| summarize NXDOMAINCount=count() by SrcIpAddr, bin(TimeGenerated, 15m)

Stage 3: where

| where NXDOMAINCount > threshold

Stage 4: join

| join kind=inner (_Im_Dns(starttime=ago(lookback), responsecodename='NXDOMAIN')
  | summarize DNSQueries = makeset(DnsQuery) by SrcIpAddr)
  on SrcIpAddr

Stage 5: extend

extend NXDOMAINthreshold

Stage 6: project-away

project-away SrcIpAddr1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
NXDOMAINCountgt
  • 200 transforms: number
field:"NXDOMAINCount" kind:gt value:"200"

Output Fields

These fields are emitted when the rule matches.

FieldSource
NXDOMAINCountsummarize
SrcIpAddrsummarize
NXDOMAINthresholdextend

Detect port misuse by static threshold (ASIM Network Session schema)

#
Status
available
Severity
medium
Time window
1h
Group by
Description, DstPortNumber, DvcAction, Name, NetworkCustomAnalyticsExists, NetworkDirection, NetworkProtocol, NetworkSummary_Protocol_Exists, Severity, Tactic, exists
Source
github.com/Azure/Azure-Sentinel

This detection rule detects port usage above the configured threshold. The rule utilize ASIM normalization, and is applied to any source which supports the ASIM Network Session schema. To tune the rule to your environment configure it using the 'NetworkSession_Monitor_Configuration' watchlist. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 156997bd-da0f-4729-b47a-0a3e02dd50c8
name: Detect port misuse by static threshold (ASIM Network Session schema)
description: |
  'This detection rule detects port usage above the configured threshold. The rule utilize [ASIM](https://aka.ms/AboutASIM) normalization, and is applied to any source which supports the ASIM Network Session schema. To tune the rule to your environment configure it using the 'NetworkSession_Monitor_Configuration' watchlist. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.'
severity: Medium
status: Available
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors:
  - connectorId: AWSS3
    dataTypes:
      - AWSVPCFlow
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftSysmonForLinux
    dataTypes:
      - Syslog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
  - connectorId: AzureNSG
    dataTypes:
      - AzureDiagnostics
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: Corelight
    dataTypes:
      - Corelight_CL
  - connectorId: AIVectraStream
    dataTypes:
      - VectraStream
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoMeraki
    dataTypes:
      - Syslog
      - CiscoMerakiNativePoller

queryFrequency: 20m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Execution
  - InitialAccess
relevantTechniques:
  - T1095  
  - T1059
  - T1203
  - T1190
query: |
  let lookback = 20m; 
  let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
      | where Type == "Detection" and ThresholdType == "Static" and Severity != "Disabled"  
      | extend
          Ports = split(Ports, ","),
          App = split(App, ","),
          Protocol = split(Protocol, ","),
          Direction = split(Direction, ","),
          Action = split(Action, ",")
      | project
          Ports,
          App,
          Protocol,
          Direction,
          Action,
          Type,
          ThresholdType,
          Threshold,
          Severity,
          Tactic,
          Name,
          Description
      | mv-expand Ports
      | mv-expand App
      | mv-expand Protocol
      | mv-expand Direction
      | mv-expand Action
      | extend
          Ports = tostring(Ports),
          App = tostring(App),
          Protocol = tostring(Protocol),
          Direction = tostring(Direction),
          Action = tostring(Action),
          Threshold = toint(Threshold);
  let NetworkSummary_Protocol = materialize(
      union isfuzzy=true 
          (
          NetworkSummary_Protocol_CL
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkSummary_Protocol_Exists = (maxv > 1),
          NetworkCustomAnalyticsExists = false
      );
  let NetworkCustomAnalytics = materialize(
      union isfuzzy=true 
          (
          NetworkCustomAnalytics_protocol_CL
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkCustomAnalyticsExists = (maxv > 1),
          NetworkSummary_Protocol_Exists = false
      );
  let allData = union isfuzzy=true 
          (
          (datatable(
      exists: int,
      NetworkSummary_Protocol_Exists: bool,
      NetworkCustomAnalyticsExists: bool
  )[
      1, false, false
  ]
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
          )
          | join (
              _Im_NetworkSession(starttime=bin(now(-20m), 20m), endtime=bin(now(), 20m))
              | where TimeGenerated > bin(now(-20m), 20m)
              | summarize Count=count()
                  by
                  NetworkProtocol,
                  DstPortNumber,
                  DstAppName,
                  NetworkDirection,
                  DvcAction,
                  bin(TimeGenerated, 20m)
              | extend
                  EventTime = TimeGenerated,
                  Count = toint(Count),
                  DstPortNumber = toint(DstPortNumber),
                  exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          (datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          )
          | join (
              NetworkCustomAnalytics_protocol_CL
              | where EventTime_t == toscalar(NetworkCustomAnalytics_protocol_CL
                  | summarize max(EventTime_t))
              | project
                  NetworkProtocol=NetworkProtocol_s,
                  DstPortNumber=DstPortNumber_d,
                  DstAppName=DstAppName_s,
                  NetworkDirection=NetworkDirection_s,
                  DvcAction=DvcAction_s,
                  Count=count__d,
                  EventTime=EventTime_t,
                  TimeGenerated,
                  Type
              | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          NetworkSummary_Protocol_CL
          | where EventTime == toscalar(NetworkSummary_Protocol_CL
              | summarize max(EventTime))
          | project
              NetworkProtocol,
              DstPortNumber,
              DstAppName,
              NetworkDirection,
              DvcAction,
              Count=count_,
              EventTime,
              TimeGenerated,
              Type
          | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
          )
      | project-away exists*, maxv*, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
  ;
  allData
  | where isnotempty(DstPortNumber)
  | summarize Sum=sum(Count) by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction 
  | join kind=inner ['mapping'] where Ports has tostring(DstPortNumber)
  | where Sum > Threshold         
      and (Protocol == "*" or Protocol has NetworkProtocol)
      and (Direction == "*" or Direction has NetworkDirection)
      and (Action == "*" or Action has DvcAction)
  | project
      Name,
      Description,
      NetworkProtocol,
      DstPortNumber,
      NetworkDirection,
      DvcAction,
      Severity,
      Tactic
  | summarize
      NetworkProtocols=make_set_if(NetworkProtocol, isnotempty(NetworkProtocol), 20), 
      NetworkDirections=make_set_if(NetworkDirection, isnotempty(NetworkDirection), 5), 
      DvcActions=make_set_if(DvcAction, isnotempty(DvcAction), 10)
      by Name, Severity, Tactic, DstPortNumber, Description
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  AllNetworkProtocols: NetworkProtocols
  AllNetworkDirections: NetworkDirections
  AllDvcAction: DvcActions
  DstPortNumber: DstPortNumber

alertDetailsOverride:
  alertDisplayNameFormat: Detected {{Name}}
  alertDescriptionFormat: '{{Description}}'
  alertTacticsColumnName: Tactic
  alertSeverityColumnName: Severity
version: 1.2.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 20m;

Let Binding: mapping

let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
    | where Type == "Detection" and ThresholdType == "Static" and Severity != "Disabled"  
    | extend
        Ports = split(Ports, ","),
        App = split(App, ","),
        Protocol = split(Protocol, ","),
        Direction = split(Direction, ","),
        Action = split(Action, ",")
    | project
        Ports,
        App,
        Protocol,
        Direction,
        Action,
        Type,
        ThresholdType,
        Threshold,
        Severity,
        Tactic,
        Name,
        Description
    | mv-expand Ports
    | mv-expand App
    | mv-expand Protocol
    | mv-expand Direction
    | mv-expand Action
    | extend
        Ports = tostring(Ports),
        App = tostring(App),
        Protocol = tostring(Protocol),
        Direction = tostring(Direction),
        Action = tostring(Action),
        Threshold = toint(Threshold);

Let Binding: NetworkSummary_Protocol

let NetworkSummary_Protocol = materialize(
    union isfuzzy=true 
        (
        NetworkSummary_Protocol_CL
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkSummary_Protocol_Exists = (maxv > 1),
        NetworkCustomAnalyticsExists = false
    );

Let Binding: NetworkCustomAnalytics

let NetworkCustomAnalytics = materialize(
    union isfuzzy=true 
        (
        NetworkCustomAnalytics_protocol_CL
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkCustomAnalyticsExists = (maxv > 1),
        NetworkSummary_Protocol_Exists = false
    );

Union isfuzzy=true (3 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: datatable(, datatable(exists:, NetworkSummary_Protocol_CL

Leg 1: datatable(

(datatable(
    exists: int,
    NetworkSummary_Protocol_Exists: bool,
    NetworkCustomAnalyticsExists: bool
)[
    1, false, false
]
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
        )
        | join (
            _Im_NetworkSession(starttime=bin(now(-20m), 20m), endtime=bin(now(), 20m))
            | where TimeGenerated > bin(now(-20m), 20m)
            | summarize Count=count()
                by
                NetworkProtocol,
                DstPortNumber,
                DstAppName,
                NetworkDirection,
                DvcAction,
                bin(TimeGenerated, 20m)
            | extend
                EventTime = TimeGenerated,
                Count = toint(Count),
                DstPortNumber = toint(DstPortNumber),
                exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*

Leg 2: datatable(exists:

(datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        )
        | join (
            NetworkCustomAnalytics_protocol_CL
            | where EventTime_t == toscalar(NetworkCustomAnalytics_protocol_CL
                | summarize max(EventTime_t))
            | project
                NetworkProtocol=NetworkProtocol_s,
                DstPortNumber=DstPortNumber_d,
                DstAppName=DstAppName_s,
                NetworkDirection=NetworkDirection_s,
                DvcAction=DvcAction_s,
                Count=count__d,
                EventTime=EventTime_t,
                TimeGenerated,
                Type
            | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*

Leg 3: NetworkSummary_Protocol_CL

NetworkSummary_Protocol_CL
        | where EventTime == toscalar(NetworkSummary_Protocol_CL
            | summarize max(EventTime))
        | project
            NetworkProtocol,
            DstPortNumber,
            DstAppName,
            NetworkDirection,
            DvcAction,
            Count=count_,
            EventTime,
            TimeGenerated,
            Type
        | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)

Applied to the Combined Result

| project-away exists*, maxv*, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists* | where isnotempty(DstPortNumber) | summarize Sum=sum(Count) by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction | join kind=inner ['mapping'] where Ports has tostring(DstPortNumber) | where Sum > Threshold         
    and (Protocol == "*" or Protocol has NetworkProtocol)
    and (Direction == "*" or Direction has NetworkDirection)
    and (Action == "*" or Action has DvcAction) | project
    Name,
    Description,
    NetworkProtocol,
    DstPortNumber,
    NetworkDirection,
    DvcAction,
    Severity,
    Tactic | summarize
    NetworkProtocols=make_set_if(NetworkProtocol, isnotempty(NetworkProtocol), 20), 
    NetworkDirections=make_set_if(NetworkDirection, isnotempty(NetworkDirection), 5), 
    DvcActions=make_set_if(DvcAction, isnotempty(DvcAction), 10)
    by Name, Severity, Tactic, DstPortNumber, Description

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Descriptionsummarize
DstPortNumbersummarize
DvcActionssummarize
Namesummarize
NetworkDirectionssummarize
NetworkProtocolssummarize
Severitysummarize
Tacticsummarize

DEV-0270 New User Creation

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

The following query tries to detect creation of a new user using a known DEV-0270 username/password schema

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 7965f0be-c039-4d18-8ee8-9a6add8aecf3
name: DEV-0270 New User Creation
description: | 
  'The following query tries to detect creation of a new user using a known DEV-0270 username/password schema'
severity: High 
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
status: Available
tactics: 
  - Persistence
relevantTechniques:
  - T1098
tags:
  - Dev-0270
query: |
  (union isfuzzy=true
  (SecurityEvent
  | where EventID == 4688
  | where CommandLine has_all ('net user', '/add') 
  | parse CommandLine with * "user " username " "*
  | extend password = extract(@"\buser\s+[^\s]+\s+([^\s]+)", 1, CommandLine) 
  | where username in('DefaultAccount') or password in('P@ssw0rd1234', '_AS_@1394') 
  | project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
  ),
  (DeviceProcessEvents 
  | where InitiatingProcessCommandLine has_all('net user', '/add') 
  | parse InitiatingProcessCommandLine with * "user " username " "* 
  | extend password = extract(@"\buser\s+[^\s]+\s+([^\s]+)", 1, InitiatingProcessCommandLine) 
  | where username in('DefaultAccount') or password in('P@ssw0rd1234', '_AS_@1394') 
  | extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
  )
  )
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.3
kind: Scheduled

Stages and Predicates

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, DeviceProcessEvents

Leg 1: SecurityEvent

SecurityEvent
| where EventID == 4688
| where CommandLine has_all ('net user', '/add') 
| parse CommandLine with * "user " username " "*
| extend password = extract(@"\buser\s+[^\s]+\s+([^\s]+)", 1, CommandLine) 
| where username in('DefaultAccount') or password in('P@ssw0rd1234', '_AS_@1394') 
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type

Leg 2: DeviceProcessEvents

DeviceProcessEvents 
| where InitiatingProcessCommandLine has_all('net user', '/add') 
| parse InitiatingProcessCommandLine with * "user " username " "* 
| extend password = extract(@"\buser\s+[^\s]+\s+([^\s]+)", 1, InitiatingProcessCommandLine) 
| where username in('DefaultAccount') or password in('P@ssw0rd1234', '_AS_@1394') 
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName

Applied to the Combined Result

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • /add transforms: term
  • net user transforms: term
field:"CommandLine" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
InitiatingProcessCommandLinematch
  • /add transforms: term corpus 2 (kusto 2)
  • net user transforms: term
field:"ParentCommandLine" kind:match
passwordin
  • P@ssw0rd1234
  • _AS_@1394
field:"password" kind:in
usernamein
  • DefaultAccount
field:"username" kind:in value:"DefaultAccount"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountextend
AccountDomainproject
Activityproject
CommandLineproject
Computerextend
EventIDproject
EventSourceNameproject
ProcessNameproject
ProcessNameFullPathproject
TimeGeneratedproject
Typeproject
passwordextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

Digital Shadows Incident Creation for include-app

#
Severity
medium
Time window
6m
Group by
EventOriginalUid
Source
github.com/Azure/Azure-Sentinel

Digital Shadows Analytic rule for generating Microsoft Sentinel incidents for the data ingested by app polling for included classifications

Rule Body

id: ede3071d-9317-45f9-b36c-6a6effee5294
name: Digital Shadows Incident Creation for include-app
description: 'Digital Shadows Analytic rule for generating Microsoft Sentinel incidents for the data ingested by app polling for included classifications'
severity: Medium
requiredDataConnectors:
  - connectorId: DigitalShadows
    dataTypes:
      - DigitalShadows_V2_CL
queryFrequency: 5m
queryPeriod: 6m
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: 
  let DSSearchLight_view  = view () {
  DigitalShadows_V2_CL
  | project-rename app_s = App, title_s = Title, raised_t = TimeRaised, updated_t = TimeUpdated, classification_s = Classification, risk_level_s = RiskLevel, risk_assessment_risk_level_s = RiskAssessmentRiskLevel, gm_link_s = GreyMatterLink, assets_s = Assets, comments_s = Comments, description_s = Description, impact_description_s = ImpactDescription, mitigation_s = Mitigation, risk_factors_s = RiskFactors, portal_id_s = PortalId, id_d = IncidentId, id_g = AlertId, status_s = Status, triage_id_g = TriageId, triage_raised_time_t = TriageRaisedTime, triage_updated_time_t = TriageUpdatedTime
  | where app_s == "include"
  | extend EventVendor="Digital Shadows", EventProduct="SearchLight",Type="DigitalShadows_V2_CL",EventStartTime=raised_t,EventMessage=title_s,EventOriginalUid=triage_id_g,EventOriginalType=classification_s
  | extend EventOriginalSeverity=iif(isempty(risk_level_s), risk_assessment_risk_level_s, risk_level_s) 
  | extend EventSeverity = case(EventOriginalSeverity == 'none', 'Informational', EventOriginalSeverity == 'very-low', 'Low',EventOriginalSeverity == 'low', 'Low',EventOriginalSeverity == 'medium', 'Medium',EventOriginalSeverity == 'high', 'High',EventOriginalSeverity == 'very-high', 'High','Informational') 
  | extend EventReportUrl = column_ifexists('gm_link_s', '')
  | extend AdditionalFields = pack("assets", assets_s, "comments", comments_s, "description", description_s, "incident_id", id_d, "alert_id", id_g, "short_code", portal_id_s, "impact", impact_description_s, "mitigation", mitigation_s, "risk_factors", risk_factors_s, "triage_status", status_s, "triage_id", triage_id_g, "triage_raised", triage_raised_time_t,"triage_updated", triage_updated_time_t, "updated", updated_t) 
  | project TimeGenerated, EventVendor,EventProduct, Type, EventStartTime,EventMessage, EventOriginalUid, EventOriginalType,EventOriginalSeverity, EventSeverity, EventReportUrl,AdditionalFields};DSSearchLight_view
  | summarize arg_max(TimeGenerated, *) by EventOriginalUid 
  | extend description = AdditionalFields.description
  | extend impact = AdditionalFields.impact
  | extend mitigation = AdditionalFields.mitigation
  | extend status = AdditionalFields.triage_status
  | extend comments = AdditionalFields.comments
entityMappings: 
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: EventReportUrl
enabled: true
suppressionDuration: 5h
suppressionEnabled: false
techniques: []
alertRuleTemplateName:
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: true
    lookbackDuration: 7d
    matchingMethod: Selected
    groupByCustomDetails:
    - triage_id
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: Digital Shadows - {{EventMessage}}
  alertDescriptionFormat: |-
    {{description}}

    {{impact}}

    {{mitigation}}
  alertSeverityColumnName: EventSeverity
customDetails:
  triage_id: EventOriginalUid
  severity: EventOriginalSeverity
  description: description
  status: status
  impact: impact
  mitigation: mitigation
version: 1.1.0
kind: Scheduled

Stages and Predicates

Let Binding: DSSearchLight_view

let DSSearchLight_view = view () { DigitalShadows_V2_CL | project-rename app_s = App, title_s = Title, raised_t = TimeRaised, updated_t = TimeUpdated, classification_s = Classification, risk_level_s = RiskLevel, risk_assessment_risk_level_s = RiskAssessmentRiskLevel, gm_link_s = GreyMatterLink, assets_s = Assets, comments_s = Comments, description_s = Description, impact_description_s = ImpactDescription, mitigation_s = Mitigation, risk_factors_s = RiskFactors, portal_id_s = PortalId, id_d = IncidentId, id_g = AlertId, status_s = Status, triage_id_g = TriageId, triage_raised_time_t = TriageRaisedTime, triage_updated_time_t = TriageUpdatedTime | where app_s == "include" | extend EventVendor="Digital Shadows", EventProduct="SearchLight",Type="DigitalShadows_V2_CL",EventStartTime=raised_t,EventMessage=title_s,EventOriginalUid=triage_id_g,EventOriginalType=classification_s | extend EventOriginalSeverity=iif(isempty(risk_level_s), risk_assessment_risk_level_s, risk_level_s) | extend EventSeverity = case(EventOriginalSeverity == 'none', 'Informational', EventOriginalSeverity == 'very-low', 'Low',EventOriginalSeverity == 'low', 'Low',EventOriginalSeverity == 'medium', 'Medium',EventOriginalSeverity == 'high', 'High',EventOriginalSeverity == 'very-high', 'High','Informational') | extend EventReportUrl = column_ifexists('gm_link_s', '') | extend AdditionalFields = pack("assets", assets_s, "comments", comments_s, "description", description_s, "incident_id", id_d, "alert_id", id_g, "short_code", portal_id_s, "impact", impact_description_s, "mitigation", mitigation_s, "risk_factors", risk_factors_s, "triage_status", status_s, "triage_id", triage_id_g, "triage_raised", triage_raised_time_t,"triage_updated", triage_updated_time_t, "updated", updated_t) | project TimeGenerated, EventVendor,EventProduct, Type, EventStartTime,EventMessage, EventOriginalUid, EventOriginalType,EventOriginalSeverity, EventSeverity, EventReportUrl,AdditionalFields};

Stage 1: source

DigitalShadows_V2_CL

Stage 2: project-rename

project-rename

Stage 3: where

where app_s == "include"

Stage 4: extend (5 consecutive steps)

extend AdditionalFields, EventMessage, EventOriginalSeverity, EventOriginalType, EventOriginalUid, EventProduct, EventReportUrl, EventSeverity, EventStartTime, EventVendor, Type

Stage 5: project

project AdditionalFields, EventMessage, EventOriginalSeverity, EventOriginalType, EventOriginalUid, EventProduct, EventReportUrl, EventSeverity, EventStartTime, EventVendor, TimeGenerated, Type

Stage 6: summarize

summarize by EventOriginalUid

Stage 7: extend (5 consecutive steps)

extend comments, description, impact, mitigation, status

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
app_seq
  • include
field:"App" kind:eq value:"include"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventOriginalUidsummarize
descriptionextend
impactextend
mitigationextend
statusextend
commentsextend

Hunt MSOL Azure AD Connect / Entra Sync servers

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

Microsoft announced that starting from April 30 2025, Microsoft Entra Connect will need to have the minimal version of 2.4.18.0. If you want to identitify if you still have an AD Connect or Entra Sync server with a lower version, you can use below KQL query.

References

Telemetry Coverage

Rule Body

DeviceTvmSoftwareInventory
| where SoftwareVendor == "microsoft"
| where SoftwareName in ("microsoft_entra_connect_sync", "microsoft_azure_ad_connect")
| distinct DeviceName, SoftwareName, SoftwareVendor, SoftwareVersion
| extend MSOnlineDepricationSafe = iff(
    parse_version(SoftwareVersion) < parse_version("2.4.18.0"),
    "No",
    "Yes"
)

Stages and Predicates

Stage 1: source

DeviceTvmSoftwareInventory

Stage 2: where

| where SoftwareVendor == "microsoft"

Stage 3: where

| where SoftwareName in ("microsoft_entra_connect_sync", "microsoft_azure_ad_connect")

Stage 4: distinct

| distinct DeviceName, SoftwareName, SoftwareVendor, SoftwareVersion

Stage 5: extend

| extend MSOnlineDepricationSafe = iff(
    parse_version(SoftwareVersion) < parse_version("2.4.18.0"),
    "No",
    "Yes"
)
MSOnlineDepricationSafe =
if/* macro: (parse_version(SoftwareVersion) < parse_version("2.4.18.0")) */"No"
else"Yes"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SoftwareNamein
  • microsoft_azure_ad_connect
  • microsoft_entra_connect_sync
field:"SoftwareName" kind:in
SoftwareVendoreq
  • microsoft
field:"SoftwareVendor" kind:eq value:"microsoft"

Output Fields

These fields are emitted when the rule matches.

FieldSource
MSOnlineDepricationSafeextend

Pathlock TDnR - ABAP Source Code Changes

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

Detects changes to ABAP source code in SAP, forwarded by Pathlock Threat Detection and Response. Unauthorized ABAP code modifications may indicate backdoor installation, persistent access mechanisms, or insider threats targeting the SAP application layer.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c01
name: Pathlock TDnR - ABAP Source Code Changes
kind: Scheduled
description: >-
  Detects changes to ABAP source code in SAP, forwarded by Pathlock Threat Detection and Response.
  Unauthorized ABAP code modifications may indicate backdoor installation, persistent access
  mechanisms, or insider threats targeting the SAP application layer.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1505
  - T1562
query: |
  Pathlock_TDnR_CL
  | where DataSource == "ABAP_CHANGES"
  | 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 == "ABAP_CHANGES"

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 - Multiple Login Sessions Detected

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

Detects events from SAP multiple login monitoring (Table USR41), forwarded by Pathlock Threat Detection and Response. Multiple concurrent sessions from different sources may indicate credential sharing, session hijacking, or compromised accounts being used simultaneously by an attacker and the legitimate user.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c73
name: Pathlock TDnR - Multiple Login Sessions Detected
kind: Scheduled
description: >-
  Detects events from SAP multiple login monitoring (Table USR41), forwarded by Pathlock Threat
  Detection and Response. Multiple concurrent sessions from different sources may indicate
  credential sharing, session hijacking, or compromised accounts being used simultaneously
  by an attacker and the legitimate user.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Discovery
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110
query: |
  Pathlock_TDnR_CL
  | where DataSource == "USER_LOGINS"
  | 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 == "USER_LOGINS"

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

Rare subscription-level operations in Azure

#
Status
available
Severity
low
Time window
1d
Group by
Caller, CallerIpAddress, OperationNameValue
Source
github.com/Azure/Azure-Sentinel

This query looks for a few sensitive subscription-level events based on Azure Activity Logs. For example, this monitors for the operation name 'Create or Update Snapshot', which is used for creating backups but could be misused by attackers to dump hashes or extract sensitive information from the disk.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Credential Access

Telemetry Coverage

Rule Body

id: 23de46ea-c425-4a77-b456-511ae4855d69
name: Rare subscription-level operations in Azure
description: |
  'This query looks for a few sensitive subscription-level events based on Azure Activity Logs. For example, this monitors for the operation name 'Create or Update Snapshot', which is used for creating backups but could be misused by attackers to dump hashes or extract sensitive information from the disk.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - Persistence
relevantTechniques:
  - T1003
  - T1098
query: |
  let starttime = 14d;
  let endtime = 1d;
  // The number of operations above which an IP address is considered an unusual source of role assignment operations
  let alertOperationThreshold = 5;
  // Add or remove operation names below as per your requirements. For operations lists, please refer to https://learn.microsoft.com/en-us/Azure/role-based-access-control/resource-provider-operations#all
  let SensitiveOperationList =  dynamic(["microsoft.compute/snapshots/write", "microsoft.network/networksecuritygroups/write", "microsoft.storage/storageaccounts/listkeys/action"]);
  let SensitiveActivity = AzureActivity
  | where OperationNameValue in~ (SensitiveOperationList) or OperationNameValue hassuffix "listkeys/action"
  | where ActivityStatusValue =~ "Success";
  SensitiveActivity
  | where TimeGenerated between (ago(starttime) .. ago(endtime))
  | summarize count() by CallerIpAddress, Caller, OperationNameValue, bin(TimeGenerated,1d)
  | where count_ >= alertOperationThreshold
  // Returns all the records from the right side that don't have matches from the left
  | join kind = rightanti (
  SensitiveActivity
  | where TimeGenerated >= ago(endtime)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_list(TimeGenerated), ActivityStatusValue = make_list(ActivityStatusValue), CorrelationIds = make_list(CorrelationId), ResourceGroups = make_list(ResourceGroup), ResourceIds = make_list(_ResourceId), ActivityCountByCallerIPAddress = count()
  by CallerIpAddress, Caller, OperationNameValue
  | where ActivityCountByCallerIPAddress >= alertOperationThreshold
  ) on CallerIpAddress, Caller, OperationNameValue
  | extend Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 2.0.3
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let alertOperationThreshold = 5;
let SensitiveOperationList = dynamic(["microsoft.compute/snapshots/write", "microsoft.network/networksecuritygroups/write", "microsoft.storage/storageaccounts/listkeys/action"]);

let SensitiveActivity is inlined into the numbered stages below.

Stages 1 to 3 define let SensitiveActivity as the rule's main pipeline source. Stages 4 to 8 use it.

Stage 1: source

AzureActivity

Stage 2: where

where (OperationNameValue endswith "listkeys/action" or OperationNameValue in~ ("microsoft.compute/snapshots/write", "microsoft.network/networksecuritygroups/write", "microsoft.storage/storageaccounts/listkeys/action"))

Stage 3: where

where ActivityStatusValue =~ "Success"

Stage 4: where

where TimeGenerated between (ago(1209600s) .. ago(86400s))

Stage 5: summarize

summarize by CallerIpAddress, Caller, OperationNameValue

Stage 6: where

where count_ >= 5

Stage 7: join

| join kind = rightanti (
SensitiveActivity
| where TimeGenerated >= ago(endtime)
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ActivityTimeStamp = make_list(TimeGenerated), ActivityStatusValue = make_list(ActivityStatusValue), CorrelationIds = make_list(CorrelationId), ResourceGroups = make_list(ResourceGroup), ResourceIds = make_list(_ResourceId), ActivityCountByCallerIPAddress = count()
by CallerIpAddress, Caller, OperationNameValue
| where ActivityCountByCallerIPAddress >= alertOperationThreshold
) on CallerIpAddress, Caller, OperationNameValue

Stage 8: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityCountByCallerIPAddressge
  • 5 transforms: number
field:"ActivityCountByCallerIPAddress" kind:ge value:"5"
ActivityStatusValueeq
  • Success corpus 2 (kusto 2)
field:"ActivityStatusValue" kind:eq value:"Success"
OperationNameValueends_with
  • listkeys/action
  • listkeys/action
field:"azure_ad::operation_name_value" kind:ends_with
OperationNameValuein
  • microsoft.compute/snapshots/write
  • microsoft.compute/snapshots/write
  • microsoft.network/networksecuritygroups/write
  • microsoft.network/networksecuritygroups/write
  • microsoft.storage/storageaccounts/listkeys/action
  • microsoft.storage/storageaccounts/listkeys/action
field:"azure_ad::operation_name_value" kind:in
count_ge
  • 5 transforms: number
field:"count_" kind:ge value:"5"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Callersummarize
CallerIpAddresssummarize
OperationNameValuesummarize
Nameextend
UPNSuffixextend

Theom - National IDs exposed

#

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
5m
Source
github.com/Azure/Azure-Sentinel

"Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0018 (Theom has observed National IDs in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"

MITRE ATT&CK Coverage

Rule Body

id: db95655e-bf5c-4c38-9676-501ec1878d4e
name: Theom - National IDs exposed
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0018 (Theom has observed National IDs in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
  - T1530
query: |
   TheomAlerts_CL
     | where customProps_RuleId_s == "TRIS0018" and (priority_s == "P1" or priority_s == "P2")
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where customProps_RuleId_s == "TRIS0018" and (priority_s == "P1" or priority_s == "P2")

Indicators

These rows show field, operator, and value matches.

TI Map Domain entity to Web Session Events (ASIM Web Session schema)

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

'This rule identifies Web Sessions for which the target URL hostname is a known IoC. This rule uses the Advanced Security Information Model (ASIM) and supports any web session source that complies with ASIM.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: afa4cb9e-6fec-4742-a17f-f494b54c01e7
name: TI Map Domain entity to Web Session Events (ASIM Web Session schema)
description: |
  'This rule identifies Web Sessions for which the target URL hostname is a known IoC. This rule uses the [Advanced Security Information Model (ASIM)](https:/aka.ms/AboutASIM) and supports any web session source that complies with ASIM.'
severity: Medium
requiredDataConnectors:
  - connectorId: SquidProxy
    dataTypes:
      - SquidProxy_CL
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
    let HAS_ANY_MAX = 10000;
    let dt_lookBack = 1h;
    let ioc_lookBack = 14d;
    //Create a list of TLDs in our threat feed for later validation
    let DOMAIN_TI=ThreatIntelIndicators
    // Picking up only IOC's that contain the entities we want
    | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
    | where IndicatorType == "domain-name"
    | extend DomainName = tolower(ObservableValue)
    | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | extend IndicatorId = tostring(split(Id, "--")[2])
    | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
    let DOMAIN_TI_list= toscalar(DOMAIN_TI | summarize NIoCs = dcount(DomainName), Domains = make_set(DomainName) 
      | project Domains=iff(NIoCs > HAS_ANY_MAX, dynamic([]), Domains) );
    DOMAIN_TI
    | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, 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 (
        _Im_WebSession(starttime=ago(dt_lookBack), url_has_any= DOMAIN_TI_list )
        //Extract domain patterns from syslog message
        | extend domain = tostring(parse_url(Url)["Host"])
        | where isnotempty(domain)
        | extend tld = tostring(split(domain, '.')[-1])
        | extend Event_TimeGenerated = TimeGenerated
    ) on $left.DomainName==$right.domain
    | where Event_TimeGenerated < ValidUntil
    | summarize Event_TimeGenerated  = arg_max(Event_TimeGenerated , *) by IndicatorId, domain
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | extend ThreatType = tostring(Data.indicator_types[0])
    | project Event_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ValidUntil, Confidence, domain, SrcIpAddr, Url, ThreatType
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
customDetails:
  EventTime: Event_TimeGenerated
  IoCDescription: Description
  ActivityGroupNames: ActivityGroupNames
  IndicatorId: IndicatorId
  ThreatType: ThreatType
  IoCExpirationTime: ValidUntil
  IoCConfidenceScore: Confidence
alertDetailsOverride:
  alertDisplayNameFormat: A web request from {{SrcIpAddr}} to hostname  {{domain}} matched an IoC
  alertDescriptionFormat: A client with address {{SrcIpAddr}} requested the URL {{Url}}, whose hostname is a known indicator of compromise of {{ThreatType}}. Consult the threat intelligence blade for more information on the indicator.
version: 1.0.12
kind: Scheduled

Stages and Predicates

Parameters

let HAS_ANY_MAX = 10000;
let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let DOMAIN_TI is inlined into the numbered stages below.

Let Binding: DOMAIN_TI_list used in Stage 9

let DOMAIN_TI_list = toscalar(DOMAIN_TI | summarize NIoCs = dcount(DomainName), Domains = make_set(DomainName) 
  | project Domains=iff(NIoCs > HAS_ANY_MAX, dynamic([]), Domains) );

Stages 1 to 7 define let DOMAIN_TI as the rule's main pipeline source. Stages 8 to 13 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "domain-name"

Stage 4: extend (4 consecutive steps)

| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend IndicatorId = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

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

DOMAIN_TI
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type

Stage 9: join

| join kind=innerunique (
    _Im_WebSession(starttime=ago(dt_lookBack), url_has_any= DOMAIN_TI_list )
    | extend domain = tostring(parse_url(Url)["Host"])
    | where isnotempty(domain)
    | extend tld = tostring(split(domain, '.')[-1])
    | extend Event_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.domain

Stage 10: where

where Event_TimeGenerated < ValidUntil

Stage 11: summarize

summarize Event_TimeGenerated by IndicatorId, domain

Stage 12: extend (3 consecutive steps)

extend ActivityGroupNames, Description, ThreatType

Stage 13: project

project ActivityGroupNames, Confidence, Description, Event_TimeGenerated, IndicatorId, SrcIpAddr, ThreatType, Url, ValidUntil, domain

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Confidenceproject
Descriptionproject
Event_TimeGeneratedproject
IndicatorIdproject
SrcIpAddrproject
ThreatTypeproject
Urlproject
ValidUntilproject
domainproject

TI Map Email entity to PaloAlto CommonSecurityLog

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

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

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 17fe80fe-072f-44d4-b62c-97a5bce56a64
name: TI Map Email entity to PaloAlto CommonSecurityLog
description: |
  'Identifies a match in CommonSecurityLog table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: ThreatIntelligence
    dataTypes:
      - 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 TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | project-reorder *, 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 (
      CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
      // Filtering PAN Logs for specific event type to match relevant email entities
      | where DeviceVendor == "Palo Alto Networks" and  DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
      | extend DestinationUserID = tolower(DestinationUserID)
      | where DestinationUserID matches regex emailregex
      | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  )
  on $left.EmailSenderAddress == $right.DestinationUserID
  | where CommonSecurityLog_TimeGenerated < ValidUntil
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id, DestinationUserID
  | project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, EmailSenderAddress, DestinationUserID, DeviceEventClassID, LogSeverity, DeviceAction, SourceIP, SourcePort,
  DestinationIP, DestinationPort, Protocol, ApplicationProtocol, Url//,  EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType,
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: DestinationUserID
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.10
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 (5 consecutive steps)

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

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 *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type

Stage 9: join

| join kind=innerunique (
    CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack) and isnotempty(DestinationUserID)
    | where DeviceVendor == "Palo Alto Networks" and  DeviceEventClassID == "wildfire" and ApplicationProtocol in ("smtp","pop3")
    | extend DestinationUserID = tolower(DestinationUserID)
    | where DestinationUserID matches regex emailregex
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.DestinationUserID

Stage 10: where

where CommonSecurityLog_TimeGenerated < ValidUntil

Stage 11: extend

extend Description

Stage 12: extend

extend ActivityGroupNames

Stage 13: summarize

summarize CommonSecurityLog_TimeGenerated by Id, DestinationUserID

Stage 14: project

project ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, Confidence, Description, DestinationIP, DestinationPort, DestinationUserID, DeviceAction, DeviceEventClassID, EmailSenderAddress, Id, LogSeverity, Protocol, SourceIP, SourcePort, Url, ValidUntil

Stage 15: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ApplicationProtocolproject
CommonSecurityLog_TimeGeneratedproject
Confidenceproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DestinationUserIDproject
DeviceActionproject
DeviceEventClassIDproject
EmailSenderAddressproject
Idproject
LogSeverityproject
Protocolproject
SourceIPproject
SourcePortproject
Urlproject
ValidUntilproject
timestampextend

TI Map Email entity to SecurityAlert

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

'Identifies a match in SecurityAlert table from any Email IOC from TI which will extend coverage to datatypes such as MCAS, StorageThreatProtection and many others'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 4b451ade-ed28-48e2-8fe7-60ae83ab2fa5
name: TI Map Email entity to SecurityAlert
description: |
  'Identifies a match in SecurityAlert table from any Email IOC from TI which will extend coverage to datatypes such as MCAS, StorageThreatProtection and many others'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert
  - 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 TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | project-reorder *, 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 (
      SecurityAlert
      | where TimeGenerated >= ago(dt_lookBack)
      | extend MSTI = case(AlertName has "TI map" and VendorName == "Microsoft" and ProductName == 'Azure Sentinel', true, false)
      | where MSTI == false
      // Converting Entities into dynamic data type and use mv-expand to unpack the array
      | extend EntitiesDynamicArray = parse_json(Entities) | mv-expand EntitiesDynamicArray
      // Parsing relevant entity column to filter type account and creating new column by combining account and UPNSuffix
      | extend Entitytype = tostring(parse_json(EntitiesDynamicArray).Type), EntityName = tostring(parse_json(EntitiesDynamicArray).Name),
      EntityUPNSuffix = tostring(parse_json(EntitiesDynamicArray).UPNSuffix)
      | where Entitytype =~ "account"
      | extend EntityEmail = tolower(strcat(EntityName, "@", EntityUPNSuffix))
      | where EntityEmail matches regex emailregex
      | extend Alert_TimeGenerated = TimeGenerated
  )
  on $left.EmailSenderAddress == $right.EntityEmail
  | where Alert_TimeGenerated < ValidUntil
  | summarize Alert_TimeGenerated = arg_max(Alert_TimeGenerated, *) by Id, AlertName
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project Alert_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
  EmailSenderAddress, EntityEmail, AlertName, AlertType,
  AlertSeverity, Entities, ProviderName, VendorName,Url//, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, 
  | extend Name = tostring(split(EntityEmail, '@', 0)[0]), UPNSuffix = tostring(split(EntityEmail, '@', 1)[0])
  | extend timestamp = Alert_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EntityEmail
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - 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 (5 consecutive steps)

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

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 *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type

Stage 9: join

| join kind=innerunique (
    SecurityAlert
    | where TimeGenerated >= ago(dt_lookBack)
    | extend MSTI = case(AlertName has "TI map" and VendorName == "Microsoft" and ProductName == 'Azure Sentinel', true, false)
    | where MSTI == false
    | extend EntitiesDynamicArray = parse_json(Entities) | mv-expand EntitiesDynamicArray
    | extend Entitytype = tostring(parse_json(EntitiesDynamicArray).Type), EntityName = tostring(parse_json(EntitiesDynamicArray).Name),
    EntityUPNSuffix = tostring(parse_json(EntitiesDynamicArray).UPNSuffix)
    | where Entitytype =~ "account"
    | extend EntityEmail = tolower(strcat(EntityName, "@", EntityUPNSuffix))
    | where EntityEmail matches regex emailregex
    | extend Alert_TimeGenerated = TimeGenerated
)
on $left.EmailSenderAddress == $right.EntityEmail

Stage 10: where

where Alert_TimeGenerated < ValidUntil

Stage 11: summarize

summarize Alert_TimeGenerated by Id, AlertName

Stage 12: extend

extend Description

Stage 13: extend

extend ActivityGroupNames

Stage 14: project

project ActivityGroupNames, AlertName, AlertSeverity, AlertType, Alert_TimeGenerated, Confidence, Description, EmailSenderAddress, Entities, EntityEmail, Id, ProviderName, Url, ValidUntil, VendorName

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
AlertNameproject
AlertSeverityproject
AlertTypeproject
Alert_TimeGeneratedproject
Confidenceproject
Descriptionproject
EmailSenderAddressproject
Entitiesproject
EntityEmailproject
Idproject
ProviderNameproject
Urlproject
ValidUntilproject
VendorNameproject
Nameextend
UPNSuffixextend
timestampextend

TI Map IP Entity to W3CIISLog

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.cIP, IndicatorId, cIP
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 5e45930c-09b1-4430-b2d1-cc75ada0dc0f
name: TI Map IP Entity to W3CIISLog
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for W3CIISLog events
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and W3CIISLog events
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        W3CIISLog
        | where TimeGenerated >= ago(dt_lookBack)
        | where isnotempty(cIP)
        | where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
        | extend W3CIISLog_TimeGenerated = TimeGenerated
    )
    on $left.TI_ipEntity == $right.cIP
    // Filter out W3CIISLog events that occurred after the expiration of the corresponding indicator
    | where W3CIISLog_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and keep the W3CIISLog event with the latest timestamp
    | summarize W3CIISLog_TimeGenerated = arg_max(W3CIISLog_TimeGenerated, *) by IndicatorId, cIP
    // Select the desired output fields
    | project timestamp = W3CIISLog_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, Computer, sSiteName, cIP, sIP, sPort, csMethod, csUserName, scStatus, scSubStatus, scWin32Status,
      NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: csUserName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: cIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.2
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 7 define let IP_Indicators as the rule's main pipeline source. Stages 8 to 11 use it.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

| where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 6: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 7: where

| where Active == true and ExpirationDateTime > now()

Stage 8: join

IP_Indicators
| join kind=innerunique (
      W3CIISLog
      | where TimeGenerated >= ago(dt_lookBack)
      | where isnotempty(cIP)
      | where ipv4_is_private(cIP) == false and cIP !startswith "fe80" and cIP !startswith "::" and cIP !startswith "127."
      | extend W3CIISLog_TimeGenerated = TimeGenerated
  )
  on $left.TI_ipEntity == $right.cIP

Stage 9: where

where W3CIISLog_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize W3CIISLog_TimeGenerated by IndicatorId, cIP

Stage 11: project

project ActivityGroupNames, Computer, ConfidenceScore, Description, EmailSourceIpAddress, ExpirationDateTime, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, TI_ipEntity, ThreatType, Type, Url, cIP, csMethod, csUserName, sIP, sPort, sSiteName, scStatus, scSubStatus, scWin32Status, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"
cIPcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:cIP
cIPstarts_with127.excludes:cIP field:"cIP" value:"127."
cIPstarts_with::excludes:cIP field:"cIP" value:"::"
cIPstarts_withfe80excludes:cIP field:"cIP" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Computerproject
ConfidenceScoreproject
Descriptionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
cIPproject
csMethodproject
csUserNameproject
sIPproject
sPortproject
sSiteNameproject
scStatusproject
scSubStatusproject
scWin32Statusproject
timestampproject

TIE Password Spraying

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

'Searches for Password spraying attacks.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: f47eb8cb-4acb-4ee4-887d-0247c6d73a72
name: TIE Password Spraying
description: |
  'Searches for Password spraying attacks.'
severity: High
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110.003
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  afad_parser
    | where MessageType == 2 and Codename == "Password Spraying"
    | 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 == "Password Spraying"

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
  • Password Spraying corpus 3 (kusto 3)
field:"Codename" kind:eq value:"Password Spraying"
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

UniFi Site Manager: Console firmware likely security-relevant

#
Status
available
Severity
high
Time window
6h
Group by
Id
Source
github.com/Azure/Azure-Sentinel

Identifies UniFi console / gateway devices (UDM, Cloud Key, USG, UXG, gateway) running firmware behind a major or minor version of the available release. Major/minor firmware jumps on consoles routinely include security advisory fixes (e.g. UniFi Security Advisory Bulletins). Patch-level updates are excluded since those are typically minor non-security bug fixes covered by the lower-severity FirmwareUpdateAvailable rule.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 4f7b9e6c-5d1a-4392-8c2b-3e4f5a6b7c8d
name: "UniFi Site Manager: Console firmware likely security-relevant"
description: |
  Identifies UniFi console / gateway devices (UDM, Cloud Key, USG, UXG, gateway) running firmware behind a major or minor version of the available release. Major/minor firmware jumps on consoles routinely include security advisory fixes (e.g. UniFi Security Advisory Bulletins). Patch-level updates are excluded since those are typically minor non-security bug fixes covered by the lower-severity FirmwareUpdateAvailable rule.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Devices_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
subTechniques: []
query: |
  // Detects consoles/gateways with a major-or-minor firmware Version gap vs the available release.
  // Patch-level gaps (e.g. 5.1.11 -> 5.1.12) are excluded - covered by FirmwareUpdateAvailable at Low severity.
  Unifi_SiteManager_Devices_CL
  | where TimeGenerated > ago(6h)
  | summarize arg_max(TimeGenerated, *) by Id
  | where IsConsole == true
  | where FirmwareStatus == "updateAvailable"
  | where isnotempty(Version) and isnotempty(UpdateAvailable)
  | extend
      currentMajor = toint(extract(@"^(\d+)\.", 1, Version)),
      currentMinor = toint(extract(@"^\d+\.(\d+)", 1, Version)),
      availableMajor = toint(extract(@"^(\d+)\.", 1, UpdateAvailable)),
      availableMinor = toint(extract(@"^\d+\.(\d+)", 1, UpdateAvailable))
  | where isnotnull(currentMajor) and isnotnull(availableMajor)
  | where (availableMajor > currentMajor) or (availableMajor == currentMajor and availableMinor > currentMinor)
  | extend GapType = iff(availableMajor > currentMajor, "major", "minor")
  | extend
      TimeGenerated = now(),
      Activity = strcat('Console ', Name, ' running ', Version, ' - likely security-relevant ', GapType, ' update available to ', UpdateAvailable)
  | project
      TimeGenerated,
      DeviceId = Id,
      DeviceName = Name,
      Model = Model,
      ProductLine = ProductLine,
      CurrentVersion = Version,
      AvailableVersion = UpdateAvailable,
      GapType,
      Activity
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
      - identifier: NetBiosName
        columnName: DeviceId
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Unifi_SiteManager_Devices_CL

Stage 2: where

| where TimeGenerated > ago(6h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by Id

Stage 4: where

| where IsConsole == true

Stage 5: where

| where FirmwareStatus == "updateAvailable"

Stage 6: where

| where isnotempty(Version) and isnotempty(UpdateAvailable)

Stage 7: extend

| extend
    currentMajor = toint(extract(@"^(\d+)\.", 1, Version)),
    currentMinor = toint(extract(@"^\d+\.(\d+)", 1, Version)),
    availableMajor = toint(extract(@"^(\d+)\.", 1, UpdateAvailable)),
    availableMinor = toint(extract(@"^\d+\.(\d+)", 1, UpdateAvailable))

Stage 8: where

| where isnotnull(currentMajor) and isnotnull(availableMajor)

Stage 9: where

| where (availableMajor > currentMajor) or (availableMajor == currentMajor and availableMinor > currentMinor)

Stage 10: extend

| extend GapType = iff(availableMajor > currentMajor, "major", "minor")
GapType =
ifavailableMajor > currentMajor"major"
else"minor"

Stage 11: extend

| extend
    TimeGenerated = now(),
    Activity = strcat('Console ', Name, ' running ', Version, ' - likely security-relevant ', GapType, ' update available to ', UpdateAvailable)

Stage 12: project

| project
    TimeGenerated,
    DeviceId = Id,
    DeviceName = Name,
    Model = Model,
    ProductLine = ProductLine,
    CurrentVersion = Version,
    AvailableVersion = UpdateAvailable,
    GapType,
    Activity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
AvailableVersionproject
CurrentVersionproject
DeviceIdproject
DeviceNameproject
GapTypeproject
Modelproject
ProductLineproject
TimeGeneratedproject

Back to all Kusto rules