Detection rules › Kusto
Kusto rules: 1z
AWSCloudTrail - Creation of Glue policy and then privilege escalation
#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
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- ASL AWS Create Policy Version to allow all resources (Splunk)
- AWS IAM CompromisedKeyQuarantine Policy Attached to User (Elastic)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - CloudFormation policy created then used for privilege escalation (Kusto)
- AWSCloudTrail - Created CRUD S3 policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD DynamoDB policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD KMS policy and then privilege escalation (Kusto)
- AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation (Kusto)
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"ErrorCode" kind:is_null | |
ErrorMessage | is_null | field:"ErrorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | summarize |
AccountUPNSuffix | summarize |
EventName | summarize |
EventSource | summarize |
PolicyName | summarize |
RecipientAccountId | summarize |
SourceIpAddress | summarize |
UserIdentityArn | summarize |
UserIdentityType | summarize |
Cisco SE - Generic IOC
#'This rule is triggered when generic IOC is observed on host.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"Generic IOC" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
HostCustomEntity | extend |
MalwareCustomEntity | extend |
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.
"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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control | |
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfidenceScore | ge |
| field:"ConfidenceScore" kind:ge value:"80" |
RecommendedActions | match |
| field:"RecommendedActions" kind:match value:"Monitor" |
Roles | contains |
| field:"Roles" kind:contains |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
pattern | contains | file:hashes | excludes:pattern field:"pattern" value:"file:hashes" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ConfidenceScore | project |
Country | project |
Description | project |
Domain | project |
IPAbuse | project |
IPv4 | project |
IPv6 | project |
IndicatorID | project |
ProductName | project |
ProviderName | project |
RecommendedActions | project |
Roles | project |
SecurityVendors | project |
Sources | project |
Tags | project |
ThreatActors | project |
ThreatType | project |
TimeGenerated | project |
URL | project |
created | project |
modified | project |
name | project |
valid_from | project |
Detect DNS queries reporting multiple errors from different clients - Anomaly Based (ASIM DNS Solution)
#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
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EventResultDetails | in |
| field:"EventResultDetails" kind:in |
EventResultDetails_s | in |
| field:"EventResultDetails_s" kind:in |
score | ge |
| field:"score" kind:ge value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsQuery | summarize |
TotalIPCount | summarize |
anomalies | extend |
baseline | extend |
score | extend |
EventTime | extend |
TotalIPs | extend |
Detect excessive NXDOMAIN DNS queries - Static threshold based (ASIM DNS Solution)
#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
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event 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.
| Field | Kind | Values | Search |
|---|---|---|---|
NXDOMAINCount | gt |
| field:"NXDOMAINCount" kind:gt value:"200" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
NXDOMAINCount | summarize |
SrcIpAddr | summarize |
NXDOMAINthreshold | extend |
Detect port misuse by static threshold (ASIM Network Session schema)
#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
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Command & Control |
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.
| Field | Source |
|---|---|
Description | summarize |
DstPortNumber | summarize |
DvcActions | summarize |
Name | summarize |
NetworkDirections | summarize |
NetworkProtocols | summarize |
Severity | summarize |
Tactic | summarize |
DEV-0270 New User Creation
#The following query tries to detect creation of a new user using a known DEV-0270 username/password schema
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
CommandLine | match |
| field:"CommandLine" kind:match |
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
InitiatingProcessCommandLine | match |
| field:"ParentCommandLine" kind:match |
password | in |
| field:"password" kind:in |
username | in |
| field:"username" kind:in value:"DefaultAccount" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | extend |
AccountDomain | project |
Activity | project |
CommandLine | project |
Computer | extend |
EventID | project |
EventSourceName | project |
ProcessName | project |
ProcessNameFullPath | project |
TimeGenerated | project |
Type | project |
password | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |
Digital Shadows Incident Creation for include-app
#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.
| Field | Kind | Values | Search |
|---|---|---|---|
app_s | eq |
| field:"App" kind:eq value:"include" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EventOriginalUid | summarize |
description | extend |
impact | extend |
mitigation | extend |
status | extend |
comments | extend |
Hunt MSOL Azure AD Connect / Entra Sync servers
#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
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceTvmSoftwareInventory action any: Installed software inventory |
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 =/* macro: (parse_version(SoftwareVersion) < parse_version("2.4.18.0")) */"No""Yes"Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
SoftwareName | in |
| field:"SoftwareName" kind:in |
SoftwareVendor | eq |
| field:"SoftwareVendor" kind:eq value:"microsoft" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
MSOnlineDepricationSafe | extend |
Pathlock TDnR - ABAP Source Code Changes
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"ABAP_CHANGES" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Pathlock TDnR - Multiple Login Sessions Detected
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DataSource | eq |
| field:"DataSource" kind:eq value:"USER_LOGINS" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AffectedUser | project |
Area | project |
Bname | project |
CentralTs | project |
DataSource | project |
DestIp | project |
Eventid | project |
Hostname | project |
Instance | project |
LogLine | project |
MessageV1 | project |
MessageV2 | project |
MessageV3 | project |
MessageV4 | project |
MsgId | project |
MsgNo | project |
MsgType | project |
Report | project |
SrcIp | project |
Subid | project |
Sysid | project |
Tcode | project |
TimeGenerated | project |
Rare subscription-level operations in Azure
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ActivityCountByCallerIPAddress | ge |
| field:"ActivityCountByCallerIPAddress" kind:ge value:"5" |
ActivityStatusValue | eq |
| field:"ActivityStatusValue" kind:eq value:"Success" |
OperationNameValue | ends_with |
| field:"azure_ad::operation_name_value" kind:ends_with |
OperationNameValue | in |
| field:"azure_ad::operation_name_value" kind:in |
count_ | ge |
| field:"count_" kind:ge value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Caller | summarize |
CallerIpAddress | summarize |
OperationNameValue | summarize |
Name | extend |
UPNSuffix | extend |
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.
"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
| Tactic | Techniques |
|---|---|
| Collection |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
customProps_RuleId_s | eq |
| field:"customProps_RuleId_s" kind:eq value:"TRIS0018" |
priority_s | eq |
| field:"priority_s" kind:eq |
TI Map Domain entity to Web Session Events (ASIM Web Session schema)
#'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
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
domain | is_not_null | field:"domain" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
Confidence | project |
Description | project |
Event_TimeGenerated | project |
IndicatorId | project |
SrcIpAddr | project |
ThreatType | project |
Url | project |
ValidUntil | project |
domain | project |
TI Map Email entity to PaloAlto CommonSecurityLog
#'Identifies a match in CommonSecurityLog table from any Email IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 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.
| Field | Kind | Values | Search |
|---|---|---|---|
ApplicationProtocol | in |
| field:"ApplicationProtocol" kind:in |
DestinationUserID | is_not_null | field:"DestinationUserID" kind:is_not_null | |
DestinationUserID | regex_match |
| field:"DestinationUserID" kind:regex_match value:"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$" |
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"wildfire" |
DeviceVendor | eq |
| field:"DeviceVendor" kind:eq value:"Palo Alto Networks" |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"email-addr" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ApplicationProtocol | project |
CommonSecurityLog_TimeGenerated | project |
Confidence | project |
Description | project |
DestinationIP | project |
DestinationPort | project |
DestinationUserID | project |
DeviceAction | project |
DeviceEventClassID | project |
EmailSenderAddress | project |
Id | project |
LogSeverity | project |
Protocol | project |
SourceIP | project |
SourcePort | project |
Url | project |
ValidUntil | project |
timestamp | extend |
TI Map Email entity to SecurityAlert
#'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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
EntityEmail | regex_match |
| field:"EntityEmail" kind:regex_match value:"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$" |
Entitytype | eq |
| field:"Entitytype" kind:eq value:"account" |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"email-addr" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
MSTI | eq |
| field:"MSTI" kind:eq value:"false" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AlertName | project |
AlertSeverity | project |
AlertType | project |
Alert_TimeGenerated | project |
Confidence | project |
Description | project |
EmailSenderAddress | project |
Entities | project |
EntityEmail | project |
Id | project |
ProviderName | project |
Url | project |
ValidUntil | project |
VendorName | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
TI Map IP Entity to W3CIISLog
#This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in W3CIISLog.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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 =isnotempty(NetworkIP)NetworkIPNetworkDestinationIPStage 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
EmailSourceIpAddress | is_not_null | field:"EmailSourceIpAddress" kind:is_not_null | |
NetworkDestinationIP | is_not_null | field:"NetworkDestinationIP" kind:is_not_null | |
NetworkIP | is_not_null | field:"NetworkIP" kind:is_not_null | |
NetworkSourceIP | is_not_null | field:"NetworkSourceIP" kind:is_not_null | |
cIP | is_not_null | field:"cIP" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
cIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:cIP |
cIP | starts_with | 127. | excludes:cIP field:"cIP" value:"127." |
cIP | starts_with | :: | excludes:cIP field:"cIP" value:"::" |
cIP | starts_with | fe80 | excludes:cIP field:"cIP" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
Computer | project |
ConfidenceScore | project |
Description | project |
EmailSourceIpAddress | project |
ExpirationDateTime | project |
IndicatorId | project |
NetworkDestinationIP | project |
NetworkIP | project |
NetworkSourceIP | project |
TI_ipEntity | project |
ThreatType | project |
Type | project |
Url | project |
cIP | project |
csMethod | project |
csUserName | project |
sIP | project |
sPort | project |
sSiteName | project |
scStatus | project |
scSubStatus | project |
scWin32Status | project |
timestamp | project |
TIE Password Spraying
#'Searches for Password spraying attacks.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"Password Spraying" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
UniFi Site Manager: Console firmware likely security-relevant
#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
| Tactic | Techniques |
|---|---|
| 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 =availableMajor > currentMajor"major""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.
| Field | Kind | Values | Search |
|---|---|---|---|
FirmwareStatus | eq |
| field:"FirmwareStatus" kind:eq value:"updateAvailable" |
IsConsole | eq |
| field:"IsConsole" kind:eq value:"true" |
UpdateAvailable | is_not_null | field:"UpdateAvailable" kind:is_not_null | |
Version | is_not_null | field:"Version" kind:is_not_null | |
availableMajor | is_not_null | field:"availableMajor" kind:is_not_null | |
currentMajor | is_not_null | field:"currentMajor" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
AvailableVersion | project |
CurrentVersion | project |
DeviceId | project |
DeviceName | project |
GapType | project |
Model | project |
ProductLine | project |
TimeGenerated | project |