Detection rules › Kusto
Kusto rules: 2p
Cisco Umbrella - Request to blocklisted file type
#'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | No specific technique |
Rule Body
id: de58ee9e-b229-4252-8537-41a4c2f4045e
name: Cisco Umbrella - Request to blocklisted file type
description: |
'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
severity: Medium
requiredDataConnectors:
- connectorId: CiscoUmbrellaDataConnector
dataTypes:
- Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
query: |
let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
let lbtime = 10m;
Cisco_Umbrella
| where TimeGenerated > ago(lbtime)
| where EventType == 'proxylogs'
| where DvcAction =~ 'Allowed'
| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
| where file_ext in (file_ext_blocklist)
| project TimeGenerated, SrcIpAddr, Identities, Filename
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Identities
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
let lbtime = 10m;
Stage 1: source
Cisco_Umbrella
Stage 2: where
| where TimeGenerated > ago(lbtime)
Stage 3: where
| where EventType == 'proxylogs'
Stage 4: where
| where DvcAction =~ 'Allowed'
Stage 5: extend
| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
Stage 6: extend
| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
Stage 7: where
| where file_ext in (file_ext_blocklist)
Stage 8: project
| project TimeGenerated, SrcIpAddr, Identities, Filename
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq value:"Allowed" |
EventType | eq |
| field:"EventType" kind:eq value:"proxylogs" |
file_ext | in |
| field:"file_ext" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Filename | project |
Identities | project |
SrcIpAddr | project |
TimeGenerated | project |
Cisco WSA - Suspected protocol abuse
#'Detects possible protocol abuse.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
Rule Body
id: 6f756792-4888-48a5-97cf-40d9430dc932
name: Cisco WSA - Suspected protocol abuse
description: |
'Detects possible protocol abuse.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
relevantTechniques:
- T1048
query: |
CiscoWSAEvent
| where DstPortNumber !in ('80', '443')
| where NetworkApplicationProtocol in~ ('HTTP', 'HTTPs')
| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = SrcUserName
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
CiscoWSAEvent
Stage 2: where
| where DstPortNumber !in ('80', '443')
Stage 3: where
| where NetworkApplicationProtocol in~ ('HTTP', 'HTTPs')
Stage 4: extend
| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = SrcUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
NetworkApplicationProtocol | in |
| field:"NetworkApplicationProtocol" kind:in |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstPortNumber | in | 443, 80 | excludes:DstPortNumber field:"DstPortNumber" value:"443" field:"DstPortNumber" value:"80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
IPCustomEntity | extend |
Cloudflare - XSS probing pattern in request
#'Detects XSS probing patterns.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 4d9d00b9-31a6-49e4-88c1-9e68277053ad
name: Cloudflare - XSS probing pattern in request
description: |
'Detects XSS probing patterns.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CloudflareDataConnector
dataTypes:
- Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let s_threshold = 3;
Cloudflare
| where HttpRequestMethod in~ ('POST', 'PUT')
| extend susp_ch = countof(ClientRequestURI, '%00')
| where ClientRequestURI matches regex @'(alert\()|(alert\%28)|(String\.fromCharCode\()|(expression\(alert)' or susp_ch > s_threshold
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: URL
fieldMappings:
- identifier: Url
columnName: CompleteUrl
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let s_threshold = 3;
Stage 1: source
Cloudflare
Stage 2: where
| where HttpRequestMethod in~ ('POST', 'PUT')
Stage 3: extend
| extend susp_ch = countof(ClientRequestURI, '%00')
Stage 4: where
| where ClientRequestURI matches regex @'(alert\()|(alert\%28)|(String\.fromCharCode\()|(expression\(alert)' or susp_ch > s_threshold
Stage 5: extend
| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientRequestURI | regex_match |
| field:"ClientRequestURI" kind:regex_match |
HttpRequestMethod | in |
| field:"HttpRequestMethod" kind:in |
susp_ch | gt |
| field:"susp_ch" kind:gt value:"3" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
susp_ch | extend |
CompleteUrl | extend |
DDoS Attack IP Addresses - PPS Threshold
#Identifies IP addresses that generates maximal traffic rate over 10k PPS during DDoS attack mitigation
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rules Detecting the Same Action
These rules filter on the same operation.
Rule Body
id: 6e76fd9d-8104-41eb-bad3-26054a3ad5f0
kind: Scheduled
name: DDoS Attack IP Addresses - PPS Threshold
description: Identifies IP addresses that generates maximal traffic rate over 10k PPS during DDoS attack mitigation
severity: Medium
status : Available
requiredDataConnectors:
- connectorId: DDOS
dataTypes:
- AzureDiagnostics
queryPeriod: 2h
queryFrequency: 2h
triggerOperator: gt
triggerThreshold: 0
query: |
AzureDiagnostics
| where ResourceType == 'PUBLICIPADDRESSES' and Category == "DDoSMitigationFlowLogs"
//sample rate of mitigation device 1:1000 so the PPS of the source is estimated to be x1000.
| summarize PPS = 1000 * count() by destPublicIpAddress_s, sec = bin(TimeGenerated, 1s), sourcePublicIpAddress_s
| summarize maxPPS = max(PPS) by destPublicIpAddress_s, sourcePublicIpAddress_s
| order by destPublicIpAddress_s, maxPPS desc
| where maxPPS > 10000
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: sourcePublicIpAddress_s
tactics:
- Impact
relevantTechniques:
- T1498
version: 1.0.1
Stages and Predicates
Stage 1: source
AzureDiagnostics
Stage 2: where
| where ResourceType == 'PUBLICIPADDRESSES' and Category == "DDoSMitigationFlowLogs"
Stage 3: summarize
| summarize PPS = 1000 * count() by destPublicIpAddress_s, sec = bin(TimeGenerated, 1s), sourcePublicIpAddress_s
Stage 4: summarize
| summarize maxPPS = max(PPS) by destPublicIpAddress_s, sourcePublicIpAddress_s
Stage 5: sort
| order by destPublicIpAddress_s, maxPPS desc
Stage 6: where
| where maxPPS > 10000
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"DDoSMitigationFlowLogs" |
ResourceType | eq |
| field:"ResourceType" kind:eq value:"PUBLICIPADDRESSES" |
maxPPS | gt |
| field:"maxPPS" kind:gt value:"10000" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
destPublicIpAddress_s | summarize |
maxPPS | summarize |
sourcePublicIpAddress_s | summarize |
Detect port misuse by anomaly based detection (ASIM Network Session schema)
#This rule detects anomalous pattern in port usage. 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: cbf07406-fa2a-48b0-82b8-efad58db14ec
name: Detect port misuse by anomaly based detection (ASIM Network Session schema)
description: |
'This rule detects anomalous pattern in port usage. 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: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
- LateralMovement
- Execution
- InitialAccess
relevantTechniques:
- T1095
- T1059
- T1203
- T1190
query: |
let lookback = 14d;
let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
| where Type == "Detection" and ThresholdType == "Anomaly" 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 AnomalyThreshold = 2.5;
let eps = materialize (_Im_NetworkSession
| project TimeGenerated
| where TimeGenerated > ago(5m)
| count
| extend Count = Count / 300);
let maxSummarizedTime = toscalar (
union isfuzzy=true
(
NetworkSummary_Protocol_CL
| where EventTime > ago(lookback)
| summarize max_TimeGenerated=max(EventTime)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
NetworkCustomAnalytics_protocol_CL
| where EventTime_t > ago(lookback)
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
print(ago(lookback))
| project max_TimeGenerated = print_0
)
| summarize maxTimeGenerated = max(max_TimeGenerated)
);
let NetworkSummary_Protocol = materialize(
union isfuzzy=true
(
NetworkSummary_Protocol_CL
| where EventTime > ago(1d)
| 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
| where EventTime_t > ago(1d)
| 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
]
| where toscalar(eps) > 500
| join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
| join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
)
| join (
_Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count()
by
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
bin(TimeGenerated, 10m)
| 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,
NetworkCustomAnalyticsExists: bool
)[
1, false, false
]
| where toscalar(eps) <= 500
| join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
| join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
)
| join (
_Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count()
by
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
bin(TimeGenerated, 10m)
| 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 > ago(lookback)
| 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 > ago(lookback)
| 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)
| make-series Total=sum(Count) on EventTime from ago(lookback) to now() step 1d by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, AnomalyThreshold, -1, 'linefit')
| mv-expand anomalies, score, baseline, EventTime, Total
| extend
anomalies = toint(anomalies),
score = toint(score),
baseline = toint(baseline),
EventTime = todatetime(EventTime),
Total = tolong(Total)
| where EventTime >= ago(1d)
| extend DstPortNumber = tostring(DstPortNumber)
| where score >= 2 * AnomalyThreshold and baseline > 0
| join kind=inner ['mapping'] where Ports == DstPortNumber
| where (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.3.0
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let AnomalyThreshold = 2.5;
Let Binding: mapping
let mapping = _GetWatchlist('NetworkSession_Monitor_Configuration')
| where Type == "Detection" and ThresholdType == "Anomaly" 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: eps
let eps = materialize (_Im_NetworkSession
| project TimeGenerated
| where TimeGenerated > ago(5m)
| count
| extend Count = Count / 300);
Let Binding: maxSummarizedTime
let maxSummarizedTime = toscalar (
union isfuzzy=true
(
NetworkSummary_Protocol_CL
| where EventTime > ago(lookback)
| summarize max_TimeGenerated=max(EventTime)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
NetworkCustomAnalytics_protocol_CL
| where EventTime_t > ago(lookback)
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
print(ago(lookback))
| project max_TimeGenerated = print_0
)
| summarize maxTimeGenerated = max(max_TimeGenerated)
);
Let Binding: NetworkSummary_Protocol
let NetworkSummary_Protocol = materialize(
union isfuzzy=true
(
NetworkSummary_Protocol_CL
| where EventTime > ago(1d)
| 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
| where EventTime_t > ago(1d)
| 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 (4 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: datatable(, datatable(, datatable(exists:, NetworkSummary_Protocol_CL
Leg 1: datatable(
(datatable(
exists: int,
NetworkSummary_Protocol_Exists: bool,
NetworkCustomAnalyticsExists: bool
)[
1, false, false
]
| where toscalar(eps) > 500
| join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
| join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
)
| join (
_Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count()
by
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
bin(TimeGenerated, 10m)
| 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(
(datatable(
exists: int,
NetworkSummary_Protocol_Exists: bool,
NetworkCustomAnalyticsExists: bool
)[
1, false, false
]
| where toscalar(eps) <= 500
| join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
| join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
)
| join (
_Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
| where TimeGenerated > maxSummarizedTime
| summarize Count=count()
by
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
bin(TimeGenerated, 10m)
| extend
EventTime = TimeGenerated,
Count = toint(Count),
DstPortNumber = toint(DstPortNumber),
exists=int(1)
)
on exists
| project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
Leg 3: 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 > ago(lookback)
| 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 4: NetworkSummary_Protocol_CL
NetworkSummary_Protocol_CL
| where EventTime > ago(lookback)
| 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) | make-series Total=sum(Count) on EventTime from ago(lookback) to now() step 1d by DstPortNumber, NetworkProtocol, NetworkDirection, DvcAction | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, AnomalyThreshold, -1, 'linefit') | mv-expand anomalies, score, baseline, EventTime, Total | extend
anomalies = toint(anomalies),
score = toint(score),
baseline = toint(baseline),
EventTime = todatetime(EventTime),
Total = tolong(Total) | where EventTime >= ago(1d) | extend DstPortNumber = tostring(DstPortNumber) | where score >= 2 * AnomalyThreshold and baseline > 0 | join kind=inner ['mapping'] where Ports == DstPortNumber | where (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 |
Failover Plan Failed
#Detects when a failover plan fails. This might indicate disaster recovery activity or issues with primary systems.
Rule Body
id: 8f339b13-02f0-400f-be18-491ec0ba71f1
name: Failover Plan Failed
description: Detects when a failover plan fails. This might indicate disaster recovery
activity or issues with primary systems.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 26110\n| extend FailoverPlanName\
\ = extract(\"FailoverPlanName=\\\"(?<FailoverPlanName>[^\\\"]*)\\\"\", 1, SyslogMessage)\n\
| project\n Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource\
\ = original_host,\n EventId = instanceId,\n UserName = user,\n [\"Failover\
\ Plan Name\"] = FailoverPlanName,\n MessageDetails = Description,\n Severity\
\ = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 26110
Stage 3: extend
| extend FailoverPlanName = extract("FailoverPlanName=\"(?<FailoverPlanName>[^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = user,
["Failover Plan Name"] = FailoverPlanName,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"26110" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
Failover Plan Name | project |
MessageDetails | project |
Severity | project |
UserName | project |
McAfee ePO - Threat was not blocked
#'Detects when a threat was not blocked on a host.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Privilege Escalation | |
| Stealth | |
| Defense Impairment |
Rule Body
id: 6d70a26a-c119-45b7-b4c6-44ac4fd1bcb7
name: McAfee ePO - Threat was not blocked
description: |
'Detects when a threat was not blocked on a host.'
severity: High
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- PrivilegeEscalation
- DefenseEvasion
relevantTechniques:
- T1562
- T1070
- T1068
- T1189
- T1195
query: |
McAfeeEPOEvent
| where ThreatActionTaken in~ ('none', 'IDS_ACTION_WOULD_BLOCK')
| extend IPCustomEntity = DvcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
McAfeeEPOEvent
Stage 2: where
| where ThreatActionTaken in~ ('none', 'IDS_ACTION_WOULD_BLOCK')
Stage 3: extend
| extend IPCustomEntity = DvcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ThreatActionTaken | in |
| field:"ThreatActionTaken" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
New Agent Added to Pool by New User or Added to a New OS Type
#'As seen in attacks such as SolarWinds attackers can look to subvert a build process by controlling build servers. Azure DevOps uses agent pools to execute pipeline tasks. An attacker could insert compromised agents that they control into the pools in order to execute malicious code. This query looks for users adding agents to pools they have not added agents to before, or adding agents to a pool of an OS that has not been added to that pool before. This detection has potential for false positives so has a configurable allow list to allow for certain users to be excluded from the logic.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Rule Body
id: 4ce177b3-56b1-4f0e-b83e-27eed4cb0b16
name: New Agent Added to Pool by New User or Added to a New OS Type
description: |
'As seen in attacks such as SolarWinds attackers can look to subvert a build process by controlling build servers. Azure DevOps uses agent pools to execute pipeline tasks.
An attacker could insert compromised agents that they control into the pools in order to execute malicious code. This query looks for users adding agents to pools they have not added agents to before, or adding agents to a pool of an OS that has not been added to that pool before. This detection has potential for false positives so has a configurable allow list to allow for certain users to be excluded from the logic.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Execution
relevantTechniques:
- T1053
query: |
let lookback = 14d;
let timeframe = 1d;
// exclude allowed users from query such as the ADO service
let allowed_users = dynamic(["Azure DevOps Service"]);
union
// Look for agents being added to a pool of a OS type not seen with that pool before
(ADOAuditLogs
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where OperationName =~ "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])
| project AgentPoolName, OsDescription
| join kind=rightanti (ADOAuditLogs
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])) on AgentPoolName, OsDescription),
// Look for users addeing agents to a pool that they have not added agents to before.
(AzureDevOpsAuditing
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| where ActorUPN !in (allowed_users)
| project AgentPoolName, ActorUPN
| join kind=rightanti (AzureDevOpsAuditing
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
) on AgentPoolName, ActorUPN)
| extend AgentName = tostring(Data.AgentName)
| extend OsDescription = tostring(Data.OsDescription)
| extend SystemDetails = Data.SystemCapabilities
| project-reorder TimeGenerated, OperationName, ScopeDisplayName, AgentPoolName, AgentName, ActorUPN, IpAddress, UserAgent, OsDescription, SystemDetails, Data
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUPN
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let lookback = 14d;
let timeframe = 1d;
let allowed_users = dynamic(["Azure DevOps Service"]);
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: ADOAuditLogs, AzureDevOpsAuditing
Leg 1: ADOAuditLogs
ADOAuditLogs
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| where OperationName =~ "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])
| project AgentPoolName, OsDescription
| join kind=rightanti (ADOAuditLogs
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| extend AgentPoolName = tostring(Data.AgentPoolName)
| extend OsDescription = tostring(Data.OsDescription)
| where isnotempty(OsDescription)
| extend OsDescription = tostring(split(OsDescription, "#", 0)[0])) on AgentPoolName, OsDescription
Leg 2: AzureDevOpsAuditing
AzureDevOpsAuditing
| where TimeGenerated > ago(lookback) and TimeGenerated < ago(timeframe)
| extend AgentPoolName = tostring(Data.AgentPoolName)
| where ActorUPN !in (allowed_users)
| project AgentPoolName, ActorUPN
| join kind=rightanti (AzureDevOpsAuditing
| where TimeGenerated > ago(timeframe)
| where OperationName == "Library.AgentAdded"
| where ActorUPN !in (allowed_users)
| extend AgentPoolName = tostring(Data.AgentPoolName)
) on AgentPoolName, ActorUPN
Applied to the Combined Result
| extend AgentName = tostring(Data.AgentName)
| extend OsDescription = tostring(Data.OsDescription)
| extend SystemDetails = Data.SystemCapabilities
| project-reorder TimeGenerated, OperationName, ScopeDisplayName, AgentPoolName, AgentName, ActorUPN, IpAddress, UserAgent, OsDescription, SystemDetails, Data
| extend timestamp = TimeGenerated
| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq |
OsDescription | is_not_null | field:"OsDescription" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
ActorUPN | eq | allowed_users | excludes:ActorUPN field:"ActorUPN" value:"allowed_users" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUPN | project |
AgentPoolName | project |
AgentName | extend |
OsDescription | extend |
SystemDetails | extend |
timestamp | extend |
AccountName | extend |
AccountUPNSuffix | extend |
Pathlock TDnR - SU24 Table USOBT_C Changes
#Detects changes to the SU24 authorization check table USOBT_C in SAP, forwarded by Pathlock Threat Detection and Response. USOBT_C defines default authorization field values for transactions; unauthorized changes may allow privilege escalation by modifying authorization check behavior.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | |
| Defense Impairment |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c18
name: Pathlock TDnR - SU24 Table USOBT_C Changes
kind: Scheduled
description: >-
Detects changes to the SU24 authorization check table USOBT_C in SAP, forwarded by Pathlock Threat
Detection and Response. USOBT_C defines default authorization field values for transactions;
unauthorized changes may allow privilege escalation by modifying authorization check behavior.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- PrivilegeEscalation
relevantTechniques:
- T1562
- T1548
query: |
Pathlock_TDnR_CL
| where DataSource == "CHANGEDOC_USOBT_C"
| 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 == "CHANGEDOC_USOBT_C"
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:"CHANGEDOC_USOBT_C" |
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 |
Ping Federate - Unexpected authentication URL.
#'Detects unexpected authentication URL.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 9578ef7f-cbb4-4e9a-bd26-37c15c53b413
name: Ping Federate - Unexpected authentication URL.
description: |
'Detects unexpected authentication URL.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
let known_domains =
PingFederateEvent
| where TimeGenerated between (ago(14d) .. (1d))
| where isnotempty(DeviceCustomString1)
| extend url_parsed = parse_url(DeviceCustomString1)
| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
| summarize makeset(url_domain);
PingFederateEvent
| where isnotempty(DeviceCustomString1)
| extend url_parsed = parse_url(DeviceCustomString1)
| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
| where url_domain !in (known_domains)
| extend AccountCustomEntity = DstUserName
| extend IpCustomEntity = SrcIpAddr
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: known_domains
let known_domains = PingFederateEvent
| where TimeGenerated between (ago(14d) .. (1d))
| where isnotempty(DeviceCustomString1)
| extend url_parsed = parse_url(DeviceCustomString1)
| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
| summarize makeset(url_domain);
Stage 1: source
PingFederateEvent
Stage 2: where
| where isnotempty(DeviceCustomString1)
Stage 3: extend
| extend url_parsed = parse_url(DeviceCustomString1)
Stage 4: extend
| extend url_domain = extract(@'.*\.(.*\.[a-z]+)', 1, tostring(url_parsed.Host))
Stage 5: where
| where url_domain !in (known_domains)
Stage 6: extend
| extend AccountCustomEntity = DstUserName
Stage 7: extend
| extend IpCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceCustomString1 | is_not_null | field:"DeviceCustomString1" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
url_domain | eq | known_domains | excludes:url_domain field:"url_domain" value:"known_domains" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
url_parsed | extend |
url_domain | extend |
AccountCustomEntity | extend |
IpCustomEntity | extend |
Service principal not using client credentials
#'This query searches for an service principal is not using a client certificate or secret is not secure. It is recommended that you review your needs and use an Authentication method for sign-in.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: D308318A-B298-4E57-82BD-74AE33C4A539
name: Service principal not using client credentials
description: |
'This query searches for an service principal is not using a client certificate or secret is not secure.
It is recommended that you review your needs and use an Authentication method for sign-in.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: SenservaPro
dataTypes:
- SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
SenservaPro_CL
| where ControlName_s == 'ServicePrincipalNotUsingClientCredentials'
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: ControlName_s
- identifier: AadTenantId
columnName: TenantId
- identifier: DisplayName
columnName: TenantDisplayName_s
- entityType: SecurityGroup
fieldMappings:
- identifier: DistinguishedName
columnName: Group_s
- entityType: AzureResource
fieldMappings:
- identifier: ResourceId
columnName: SourceSystem
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
SenservaPro_CL
Stage 2: where
| where ControlName_s == 'ServicePrincipalNotUsingClientCredentials'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ControlName_s | eq |
| field:"ControlName_s" kind:eq value:"ServicePrincipalNotUsingClientCredentials" |
Suspicious AWS console logins by credential access alerts
#This query aims to detect instances of successful AWS console logins that align with high-severity credential access or Initial Access alerts generated by Defender Products. Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | IdentityInfo action any: Identity information |
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS Console Login (Panther)
- AWS Console Login Failed During MFA Challenge (Splunk)
- AWS ConsoleLogin Failed Authentication (Sigma)
- AWS CreateLoginProfile (Splunk)
- AWS Credential Access Failed Login (Splunk)
- AWS High Number Of Failed Authentications For User (Splunk)
- AWS High Number Of Failed Authentications From Ip (Splunk)
- AWS Multiple Failed MFA Requests For User (Splunk)
Rule Body
id: b51fe620-62ad-4ed2-9d40-5c97c0a8231f
name: Suspicious AWS console logins by credential access alerts
description: |
'This query aims to detect instances of successful AWS console logins that align with high-severity credential access or Initial Access alerts generated by Defender Products.
Specifically, it focuses on scenarios where the successful login takes place within a 60-minute timeframe of the high-severity alert. The login is considered relevant if it originates from an IP address associated with potential attackers.'
severity: Medium
requiredDataConnectors:
- connectorId: OfficeATP
dataTypes:
- SecurityAlert
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: MicrosoftDefenderAdvancedThreatProtection
dataTypes:
- SecurityAlert
- connectorId: AzureActiveDirectoryIdentityProtection
dataTypes:
- SecurityAlert (IPC)
- connectorId: BehaviorAnalytics
dataTypes:
- IdentityInfo
- connectorId: MicrosoftThreatProtection
dataTypes:
- SecurityAlert
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1078
query: |
SecurityAlert
// Filtering alerts based on Microsoft product names
| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security","Azure Active Directory Identity Protection", "Microsoft Defender ATP")
// Narrowing down alerts to specific tactics
| where Tactics in("CredentialAccess", "InitialAccess")
// Focusing on high-severity alerts
| where AlertSeverity == "High"
// Parsing and extending the 'Entities' column as JSON objects
| extend Entities = parse_json(Entities)
// Exploring IP entities within the alert entities
| mv-apply Entity = Entities on
(
where Entity.Type == 'ip'
| extend EntityIp = tostring(Entity.Address)
)
// Exploring account entities within the alert entities
| mv-apply Entity = Entities on
(
where Entity.Type == 'account'
| extend AccountObjectId = tostring(Entity.AadUserId)
)
// Filtering out alerts with missing IP or account information
| where isnotempty(EntityIp) and isnotempty(AccountObjectId)
// Summarizing relevant fields for further analysis
| summarize
by
AlertName,
ProductName,
ProviderName,
AlertSeverity,
EntityIp,
Tactics,
Techniques,
AlertTime= bin(TimeGenerated, 1min),
AccountObjectId,
AlertTimeGenerated=TimeGenerated
// Joining with IdentityInfo to obtain additional account details
| join kind=inner (
IdentityInfo
| where TimeGenerated >= ago(1d)
| distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
)
on AccountObjectId
|extend Name = tostring(split(AccountUPN,'@')[0]), UPNSuffix =tostring(split(AccountUPN,'@')[1])
// Joining with AWSCloudTrail data to correlate AWS console logins
| join kind=inner (
AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
| extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)
| where ActionType == "Success"
| extend AWSTime= bin(TimeGenerated, 1min)
| project
EventName,
EventSource,
EventTypeName,
RecipientAccountId,
ResponseElements,
SessionMfaAuthenticated,
SourceIpAddress,
TimeGenerated,
UserAgent,
UserIdentityArn,
UserIdentityType,
CTUPN,
AWSTime,
UserIdentityUserName
)
on $left.EntityIp == $right.SourceIpAddress
// Filtering login event after the Alert generation time
| where AlertTimeGenerated >= AWSTime
// Calculating the time difference between alert generation and AWS login
| extend timediff = datetime_diff('minute', AlertTimeGenerated, TimeGenerated)
// Filtering alerts with a time difference of up to 60 minutes
| where timediff between ((-60)..(60))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountUPN
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
AWSUSerUPN: CTUPN
AzureUserUPN: AccountUPN
ComonIp: SourceIpAddress
UserAgent: UserAgent
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
SecurityAlert
Stage 2: where
| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security","Azure Active Directory Identity Protection", "Microsoft Defender ATP")
Stage 3: where
| where Tactics in("CredentialAccess", "InitialAccess")
Stage 4: where
| where AlertSeverity == "High"
Stage 5: extend
| extend Entities = parse_json(Entities)
Stage 6: kusto:mv-apply
| mv-apply Entity = Entities on
(
where Entity.Type == 'ip'
| extend EntityIp = tostring(Entity.Address)
)
Stage 7: kusto:mv-apply
| mv-apply Entity = Entities on
(
where Entity.Type == 'account'
| extend AccountObjectId = tostring(Entity.AadUserId)
)
Stage 8: where
| where isnotempty(EntityIp) and isnotempty(AccountObjectId)
Stage 9: summarize
| summarize
by
AlertName,
ProductName,
ProviderName,
AlertSeverity,
EntityIp,
Tactics,
Techniques,
AlertTime= bin(TimeGenerated, 1min),
AccountObjectId,
AlertTimeGenerated=TimeGenerated
Stage 10: join
| join kind=inner (
IdentityInfo
| where TimeGenerated >= ago(1d)
| distinct AccountObjectId, AccountUPN=tolower(AccountUPN)
)
on AccountObjectId
Stage 11: extend
extend Name, UPNSuffix
Stage 12: join
| join kind=inner (
AWSCloudTrail
| where EventName == "ConsoleLogin"
| extend CTUPN= tolower(tostring(tolower(split(UserIdentityArn, "/", 2)[0])))
| extend ActionType= tostring(parse_json(ResponseElements).ConsoleLogin)
| where ActionType == "Success"
| extend AWSTime= bin(TimeGenerated, 1min)
| project
EventName,
EventSource,
EventTypeName,
RecipientAccountId,
ResponseElements,
SessionMfaAuthenticated,
SourceIpAddress,
TimeGenerated,
UserAgent,
UserIdentityArn,
UserIdentityType,
CTUPN,
AWSTime,
UserIdentityUserName
)
on $left.EntityIp == $right.SourceIpAddress
Stage 13: where
where AlertTimeGenerated >= AWSTime
Stage 14: extend
extend timediff
Stage 15: where
where /* macro: (timediff between ((- 60) .. 60)) */
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AccountObjectId | is_not_null | field:"AccountObjectId" kind:is_not_null | |
ActionType | eq |
| field:"ActionType" kind:eq value:"Success" |
AlertSeverity | eq |
| field:"AlertSeverity" kind:eq value:"High" |
Entity.Type | eq |
| field:"Entity.Type" kind:eq |
EntityIp | is_not_null | field:"EntityIp" kind:is_not_null | |
EventName | eq |
| field:"EventName" kind:eq value:"ConsoleLogin" |
ProductName | in |
| field:"ProductName" kind:in |
Tactics | in |
| field:"Tactics" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountObjectId | summarize |
AlertName | summarize |
AlertSeverity | summarize |
AlertTime | summarize |
AlertTimeGenerated | summarize |
EntityIp | summarize |
ProductName | summarize |
ProviderName | summarize |
Tactics | summarize |
Techniques | summarize |
Name | extend |
UPNSuffix | extend |
timediff | extend |
Suspicious Login from deleted guest account
#This query will detect logins from guest account which was recently deleted. For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Delete user |
| Entra ID | SigninLogs sign-in event: Successful sign-in |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Authentication Method Changed for Privileged Account (Kusto)
- [Entra ID] Suspicious Continuous OAuth Token Usage (Kusto)
- Account Created and Deleted in Short Timeframe (Kusto)
- Account Created And Deleted Within A Close Time Frame (Sigma)
- Account created from non-approved sources (Kusto)
- Account created or deleted by non-approved user (Kusto)
- Addition of a Temporary Access Pass to a Privileged Account (Kusto)
- Anomalous sign-in location by user account and authenticating application (Kusto)
Rule Body
id: defe4855-0d33-4362-9557-009237623976
name: Suspicious Login from deleted guest account
description: |
' This query will detect logins from guest account which was recently deleted.
For any successful logins from deleted identities should be investigated further if any existing user accounts have been altered or linked to such identity prior deletion'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- GuestorExternalIdentities
query: |
let query_frequency = 1h;
let query_period = 1d;
AuditLogs
| where TimeGenerated > ago(query_frequency)
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
| mv-expand TargetResource = TargetResources
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
| extend
Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(query_period)
| where ResultType == 0
| summarize take_any(*) by UserPrincipalName
| extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
| project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
| where SigninLogs_TimeGenerated > Delete_TimeGenerated
| project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
| extend
AccountName = tostring(split(UserPrincipalName, "@")[0]),
AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
version: 1.0.5
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let query_frequency = 1h;
let query_period = 1d;
Stage 1: source
AuditLogs
Stage 2: where
| where TimeGenerated > ago(query_frequency)
Stage 3: where
| where Category =~ "UserManagement" and OperationName =~ "Delete user"
Stage 4: mv-expand
| mv-expand TargetResource = TargetResources
Stage 5: where
| where TargetResource["type"] == "User" and TargetResource["userPrincipalName"] has "#EXT#"
Stage 6: extend
| extend ParsedDeletedUserPrincipalName = extract(@"^[0-9a-f]{32}([^\#]+)\#EXT\#", 1, tostring(TargetResource["userPrincipalName"]))
Stage 7: extend
| extend
Initiator = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["displayName"]), tostring(InitiatedBy["user"]["userPrincipalName"])),
InitiatorId = iif(isnotempty(InitiatedBy["app"]), tostring(InitiatedBy["app"]["servicePrincipalId"]), tostring(InitiatedBy["user"]["id"])),
Delete_IPAddress = tostring(InitiatedBy[tostring(bag_keys(InitiatedBy)[0])]["ipAddress"])
Stage 8: project
| project Delete_TimeGenerated = TimeGenerated, Category, Identity, Initiator, Delete_IPAddress, OperationName, Result, ParsedDeletedUserPrincipalName, InitiatedBy, AdditionalDetails, TargetResources, InitiatorId, CorrelationId
Stage 9: join
| join kind=inner (
SigninLogs
| where TimeGenerated > ago(query_period)
| where ResultType == 0
| summarize take_any(*) by UserPrincipalName
| extend ParsedUserPrincipalName = translate("@", "_", UserPrincipalName)
| project SigninLogs_TimeGenerated = TimeGenerated, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, IPAddress, LocationDetails, AppDisplayName, ResourceDisplayName, ClientAppUsed, UserAgent, DeviceDetail, UserId, UserType, OriginalRequestId, ParsedUserPrincipalName
) on $left.ParsedDeletedUserPrincipalName == $right.ParsedUserPrincipalName
Stage 10: where
where SigninLogs_TimeGenerated > Delete_TimeGenerated
Stage 11: project-away
project-away ParsedDeletedUserPrincipalName, ParsedUserPrincipalName
Stage 12: extend
extend AccountName, AccountUPNSuffix
Stage 13: summarize aggregation inside the join branch
summarize by UserPrincipalName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"UserManagement" |
OperationName | eq |
| field:"OperationName" kind:eq value:"Delete user" |
ResultType | eq |
| field:"ResultType" kind:eq value:"0" |
TargetResource["type"] | eq |
| |
TargetResource["userPrincipalName"] | match |
|
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UserPrincipalName | summarize |
Tenant Quota Changed
#Detects when a tenant quota is updated.
Rule Body
id: da25b390-b97a-4ea4-abe3-5bbd7f90642c
name: Tenant Quota Changed
description: Detects when a tenant quota is updated.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 24160\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n MessageDetails = Description,\n\
\ Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 24160
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"24160" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
TI Map IP Entity to DeviceNetworkEvents
#Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceNetworkEvents action any: Network activity |
Rule Body
id: b2df4979-d34a-48b3-a7d9-f473a4bf8058
name: TI Map IP Entity to DeviceNetworkEvents
description: |
'Identifies a match in DeviceNetworkEvents Event data from any IP Indicator from TI.'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteIP)
| where TimeGenerated > ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend isPrivate = ipv4_is_private(RemoteIP)
| where isPrivate != true;
let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);
ThreatIntelligenceIndicator
| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
| where TimeGenerated >= ago(ioc_lookBack)
| extend TI_ipEntity = coalesce(NetworkIP, EmailSourceIpAddress, NetworkDestinationIP, NetworkSourceIP)
| where TI_ipEntity in (IPs)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
| where Active == true and ExpirationDateTime > now()
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
| join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity == $right.RemoteIP
| summarize TimeGenerated = arg_max(TimeGenerated, *) by IndicatorId, TI_ipEntity, DeviceName
// DeviceName, TI_ipEntity, RemoteUrl, InitiatingProcessAccountUpn
| extend timestamp = TimeGenerated, Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: TI_ipEntity
- entityType: URL
fieldMappings:
- identifier: Url
columnName: RemoteUrl
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: DeviceName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: DeviceNetworkEvents_
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteIP)
| where TimeGenerated > ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend isPrivate = ipv4_is_private(RemoteIP)
| where isPrivate != true;
Let Binding: IPs
let IPs = DeviceNetworkEvents_ | distinct RemoteIP | summarize make_list(RemoteIP);
Stage 1: source
let DeviceNetworkEvents_
Stage 2: source
let IPs
Stage 3: source
ThreatIntelligenceIndicator
Stage 4: where
where (isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkIP) or isnotempty(NetworkSourceIP))
Stage 5: where
where TimeGenerated >= ago(1209600s)
Stage 6: extend
extend TI_ipEntity
Stage 7: where
where TI_ipEntity == "IPs"
Stage 8: summarize
summarize LatestIndicatorTime by IndicatorId
Stage 9: where
where Active == true
Stage 10: where
where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")
Stage 11: join
join kind=innerunique (DeviceNetworkEvents_) on $left.TI_ipEntity, $right.RemoteIP
Stage 12: summarize
summarize TimeGenerated by IndicatorId, TI_ipEntity, DeviceName
Stage 13: extend
extend Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
ActionType | match | ConnectionFailed | excludes:ActionType field:"ActionType" value:"ConnectionFailed" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceName | summarize |
IndicatorId | summarize |
TI_ipEntity | summarize |
TimeGenerated | summarize |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
TI Map IP entity to Workday(ASimAuditEventLogs)
#Detects a match in Workday activity from any IP Indicator of Compromise (IOC) provided by Threat Intelligence (TI).
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 92e8e945-6e99-4e4b-bef8-468b4c19fc3a
name: TI Map IP entity to Workday(ASimAuditEventLogs)
description: |
Detects a match in Workday activity from any IP Indicator of Compromise (IOC) provided by Threat Intelligence (TI).
severity: Medium
requiredDataConnectors:
- connectorId: ThreatIntelligence
dataTypes:
- ThreatIntelIndicators
- connectorId: ThreatIntelligenceTaxii
dataTypes:
- ThreatIntelIndicators
- connectorId: Workday
dataTypes:
- Workday
- connectorId: MicrosoftDefenderThreatIntelligence
dataTypes:
- ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dtLookBack = 1h; // Define the lookback period for audit events
let ioc_lookBack = 14d; // Define the lookback period for threat intelligence indicators
ThreatIntelIndicators
| where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
//extract key part of kv pair
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
| extend NetworkSourceIP = toupper(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where isnotempty(NetworkSourceIP)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue // Get the latest indicator time for each IndicatorId
| extend TI_ipEntity = coalesce(NetworkSourceIP, NetworkSourceIP, NetworkSourceIP, NetworkSourceIP) // Combine IP fields into a single entity
| where IsActive and (ValidUntil > now() or isempty(ValidUntil)) // Filter for active indicators that have not expired
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
| join kind=inner (
ASimAuditEventLogs
| where EventVendor == "Workday" // Filter for Workday events
| where TimeGenerated >= ago(dtLookBack) // Filter events within the lookback period
| where isnotempty(DvcIpAddr) // Filter for events with a device IP address
| extend WD_TimeGenerated = EventStartTime // Rename the event start time column
| project WD_TimeGenerated, ActorUsername, DvcIpAddr, Operation, Object // Select relevant columns
)
on $left.TI_ipEntity == $right.DvcIpAddr // Join on the IP entity
| extend Description = tostring(parse_json(Data).description)
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project
LatestIndicatorTime,
Description,
ActivityGroupNames,
Id,
ValidUntil,
Confidence,
WD_TimeGenerated,
ActorUsername,
DvcIpAddr,
Operation,
Object // Select relevant columns after the join
| extend
timestamp = WD_TimeGenerated,
Name = tostring(split(ActorUsername, '@', 0)),
UPNSuffix = tostring(split(ActorUsername, '@', 1)) // Add additional fields for timestamp, name, and UPN suffix
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: DvcIpAddr
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let dtLookBack = 1h;
let ioc_lookBack = 14d;
Stage 1: source
ThreatIntelIndicators
Stage 2: where
| where TimeGenerated >= ago(ioc_lookBack)
Stage 3: extend
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
Stage 4: where
| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
Stage 5: extend
| extend NetworkSourceIP = toupper(ObservableValue)
Stage 6: extend
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
Stage 7: where
| where isnotempty(NetworkSourceIP)
Stage 8: summarize
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
Stage 9: extend
| extend TI_ipEntity = coalesce(NetworkSourceIP, NetworkSourceIP, NetworkSourceIP, NetworkSourceIP)
Stage 10: where
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
Stage 11: project-reorder
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
Stage 12: join
| join kind=inner (
ASimAuditEventLogs
| where EventVendor == "Workday"
| where TimeGenerated >= ago(dtLookBack)
| where isnotempty(DvcIpAddr)
| extend WD_TimeGenerated = EventStartTime
| project WD_TimeGenerated, ActorUsername, DvcIpAddr, Operation, Object
)
on $left.TI_ipEntity == $right.DvcIpAddr
Stage 13: extend
extend Description
Stage 14: extend
extend ActivityGroupNames
Stage 15: project
project ActivityGroupNames, ActorUsername, Confidence, Description, DvcIpAddr, Id, LatestIndicatorTime, Object, Operation, ValidUntil, WD_TimeGenerated
Stage 16: extend
extend Name, UPNSuffix, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcIpAddr | is_not_null | field:"DvcIpAddr" kind:is_not_null | |
EventVendor | eq |
| field:"EventVendor" kind:eq value:"Workday" |
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
NetworkSourceIP | is_not_null | field:"NetworkSourceIP" kind:is_not_null | |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ActorUsername | project |
Confidence | project |
Description | project |
DvcIpAddr | project |
Id | project |
LatestIndicatorTime | project |
Object | project |
Operation | project |
ValidUntil | project |
WD_TimeGenerated | project |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
UniFi Site Manager: Pending firmware updates outstanding for 7d+
#Identifies sites with pending firmware updates outstanding continuously for the last 7 days, indicating patch debt that should be scheduled.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Rule Body
id: d943d401-861e-7186-d42c-c505fbf7c619
name: "UniFi Site Manager: Pending firmware updates outstanding for 7d+"
description: |
Identifies sites with pending firmware updates outstanding continuously for the last 7 days, indicating patch debt that should be scheduled.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Sites_CL
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1595
subTechniques: []
query: |
Unifi_SiteManager_Sites_CL
| where TimeGenerated > ago(7d)
| extend pendingUpdates = PendingUpdateDevices
| summarize MinPending = min(pendingUpdates), MaxPending = max(pendingUpdates), arg_max(TimeGenerated, pendingUpdates) by SiteId, SiteName
| where MinPending > 0
| extend Activity = strcat('Pending updates sustained - current ', pendingUpdates, ' device(s)')
| project TimeGenerated, SiteId, SiteName, Activity, PendingDevices = pendingUpdates
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SiteId
- identifier: DnsDomain
columnName: SiteName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: P1D
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Stage 1: source
Unifi_SiteManager_Sites_CL
Stage 2: where
| where TimeGenerated > ago(7d)
Stage 3: extend
| extend pendingUpdates = PendingUpdateDevices
Stage 4: summarize
| summarize MinPending = min(pendingUpdates), MaxPending = max(pendingUpdates), arg_max(TimeGenerated, pendingUpdates) by SiteId, SiteName
Stage 5: where
| where MinPending > 0
Stage 6: extend
| extend Activity = strcat('Pending updates sustained - current ', pendingUpdates, ' device(s)')
Stage 7: project
| project TimeGenerated, SiteId, SiteName, Activity, PendingDevices = pendingUpdates
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
MinPending | gt |
| field:"MinPending" kind:gt value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
PendingDevices | project |
SiteId | project |
SiteName | project |
TimeGenerated | project |
Zero Networks Segment - New API Token created
#'Detects when a api token has been created.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 603a6b18-b54a-43b7-bb61-d2b0b47d224a
name: Zero Networks Segment - New API Token created
description: |
'Detects when a api token has been created.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: ZeroNetworksSegmentAuditFunction
dataTypes:
- ZNSegmentAudit_CL
- connectorId: ZeroNetworksSegmentAuditNativePoller
dataTypes:
- ZNSegmentAuditNativePoller_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1528
query: |
ZNSegmentAudit
| where AuditTypeId == 25
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: PerformedByName
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
ZNSegmentAudit
Stage 2: where
| where AuditTypeId == 25
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AuditTypeId | eq |
| field:"AuditTypeId" kind:eq value:"25" |