Detection rules › Kusto
Kusto rules: 28
1Password - Changes to firewall rules
#This will alert when changes have been made to the firewall rules. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | account-updatfw: Update Firewall Rules |
Rule Body
id: 54e6bb8e-2935-422f-9387-dba1961abfd7
name: 1Password - Changes to firewall rules
version: 1.0.0
kind: Scheduled
description: |-
This will alert when changes have been made to the firewall rules. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.
Ref: https://1password.com/
Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
- connectorId: 1Password
dataTypes:
- OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |-
OnePasswordEventLogs_CL
| where log_source == "auditevents"
| where action == "updatfw"
| where object_type == "account"
| extend
ActorUsername = actor_details.email
, SrcIpAddr = session.ip
subTechniques:
- T1562.007
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
Stages and Predicates
Stage 1: source
OnePasswordEventLogs_CL
Stage 2: where
| where log_source == "auditevents"
Stage 3: where
| where action == "updatfw"
Stage 4: where
| where object_type == "account"
Stage 5: extend
| extend
ActorUsername = actor_details.email
, SrcIpAddr = session.ip
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | eq |
| field:"action" kind:eq value:"updatfw" |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq value:"account" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
SrcIpAddr | extend |
Anomaly found in Network Session Traffic (ASIM Network Session schema)
#The rule identifies anomalous pattern in network session traffic based on previously seen data, different Device Action, Network Protocol, Network Direction or overall volume. The rule utilize ASIM normalization, and is applied to any source which supports the ASIM Network Session schema. 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 |
|---|---|
| Discovery | |
| Lateral Movement | |
| Command & Control | |
| Exfiltration |
Telemetry Coverage
Rule Body
id: cd6def0d-3ef0-4d55-a7e3-faa96c46ba12
name: Anomaly found in Network Session Traffic (ASIM Network Session schema)
description: |
'The rule identifies anomalous pattern in network session traffic based on previously seen data, different Device Action, Network Protocol, Network Direction or overall volume. The rule utilize [ASIM](https://aka.ms/AboutASIM) normalization, and is applied to any source which supports the ASIM Network Session schema. 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
- Discovery
- Exfiltration
- LateralMovement
relevantTechniques:
- T1095
- T1071
- T1046
- T1030
- T1210
query: |
let min_t = ago(14d);
let max_t = now();
let dt = 1d;
let fieldForDvcAction = "DvcAction";
let fieldForNetworkDirection = "NetworkDirection";
let fieldForNetworkProtocol = "NetworkProtocol";
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 > min_t
| summarize max_TimeGenerated=max(EventTime)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
NetworkCustomAnalytics_protocol_CL
| where EventTime_t > min_t
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
print(min_t)
| 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 > min_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 > min_t
| project
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
Count=count_,
EventTime,
TimeGenerated,
Type
| extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
)
;
let findVolumneBasedAnomaly = allData
| make-series total=sum(Count) on EventTime from min_t to max_t step dt
| 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)
| where score >= 2 * AnomalyThreshold
;
let findAnomalies = (field: string) {
allData
| where isnotempty(column_ifexists(field, ""))
| make-series total=sum(Count) on EventTime from min_t to max_t step dt by column_ifexists(field, "")
| 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)
| where score >= 2 * AnomalyThreshold
};
union
findAnomalies(fieldForDvcAction),
findAnomalies(fieldForNetworkDirection),
findAnomalies(fieldForNetworkProtocol),
findVolumneBasedAnomaly
| extend
anomalyFieldType = case (
isnotempty(column_ifexists(fieldForDvcAction, "")),
"DvcAction",
isnotempty(column_ifexists(fieldForNetworkDirection, "")),
"NetworkDirection",
isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
"NetworkProtocol",
"TotalVolume"
),
anomalyFieldValue = case (
isnotempty(column_ifexists(fieldForDvcAction, "")),
column_ifexists(fieldForDvcAction, ""),
isnotempty(column_ifexists(fieldForNetworkDirection, "")),
column_ifexists(fieldForNetworkDirection, ""),
isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
column_ifexists(fieldForNetworkProtocol, ""),
"Overall"
)
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
AnomalyFieldType: anomalyFieldType
AnomalyFieldValue: anomalyFieldValue
Score: score
alertDetailsOverride:
alertDisplayNameFormat: Anomaly was observed with {{anomalyFieldValue}} Traffic
alertDescriptionFormat: 'Based on past data, anomaly was observed in {{anomalyFieldValue}} Traffic with a score of {{score}}.'
version: 1.3.0
kind: Scheduled
Stages and Predicates
Parameters
let min_t = ago(14d);
let max_t = now();
let dt = 1d;
let fieldForDvcAction = "DvcAction";
let fieldForNetworkDirection = "NetworkDirection";
let fieldForNetworkProtocol = "NetworkProtocol";
let AnomalyThreshold = 2.5;
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 > min_t
| summarize max_TimeGenerated=max(EventTime)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
NetworkCustomAnalytics_protocol_CL
| where EventTime_t > min_t
| summarize max_TimeGenerated=max(EventTime_t)
| extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
),
(
print(min_t)
| 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
);
Let Binding: allData
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 > min_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 > min_t
| project
NetworkProtocol,
DstPortNumber,
DstAppName,
NetworkDirection,
DvcAction,
Count=count_,
EventTime,
TimeGenerated,
Type
| extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
);
Let Binding: findVolumneBasedAnomaly
let findVolumneBasedAnomaly = allData
| make-series total=sum(Count) on EventTime from min_t to max_t step dt
| 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)
| where score >= 2 * AnomalyThreshold;
Let Binding: findAnomalies
let findAnomalies = (field: string) {
allData
| where isnotempty(column_ifexists(field, ""))
| make-series total=sum(Count) on EventTime from min_t to max_t step dt by column_ifexists(field, "")
| 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)
| where score >= 2 * AnomalyThreshold
};
Union (4 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: findAnomalies, findAnomalies, findAnomalies, findVolumneBasedAnomaly
Leg 1: findAnomalies
findAnomalies(fieldForDvcAction)
Leg 2: findAnomalies
findAnomalies(fieldForNetworkDirection)
Leg 3: findAnomalies
findAnomalies(fieldForNetworkProtocol)
Leg 4: findVolumneBasedAnomaly
Applied to the Combined Result
| extend
anomalyFieldType = case (
isnotempty(column_ifexists(fieldForDvcAction, "")),
"DvcAction",
isnotempty(column_ifexists(fieldForNetworkDirection, "")),
"NetworkDirection",
isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
"NetworkProtocol",
"TotalVolume"
),
anomalyFieldValue = case (
isnotempty(column_ifexists(fieldForDvcAction, "")),
column_ifexists(fieldForDvcAction, ""),
isnotempty(column_ifexists(fieldForNetworkDirection, "")),
column_ifexists(fieldForNetworkDirection, ""),
isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
column_ifexists(fieldForNetworkProtocol, ""),
"Overall"
)
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
score | ge |
| field:"score" kind:ge value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
total | extend |
anomalies | extend |
baseline | extend |
score | extend |
EventTime | extend |
anomalyFieldType | extend |
anomalyFieldValue | extend |
App GW WAF - Code Injection
#'Identifies a match for a Code Injection based attack in the App Gateway WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements. References: https://owasp.org/www-community/attacks/Code_Injection'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Execution | |
| Privilege Escalation |
Rule Body
id: 912a18fc-6165-446b-8740-81ae6c3f75ee
name: App GW WAF - Code Injection
description: |
'Identifies a match for a Code Injection based attack in the App Gateway WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements.
References: https://owasp.org/www-community/attacks/Code_Injection'
severity: High
status: Available
requiredDataConnectors:
- connectorId: WAF
dataTypes:
- AzureDiagnostics
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Execution
- InitialAccess
- PrivilegeEscalation
relevantTechniques:
- T1548
- T1203
- T1190
- T1548
tags:
- Code Injection
query: |
let Threshold = 3;
AGWFirewallLogs
| where Action == "Matched"
| where Message has "Injection" or Message has "File Inclusion"
| project TransactionId, Hostname, RequestUri, TimeGenerated, ClientIp, Message, DetailedMessage, DetailedData
| join kind = inner(
AGWFirewallLogs
| where Action == "Blocked") on TransactionId
| extend Uri = strcat(Hostname,RequestUri)
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), TransactionID = make_set(TransactionId,100), Message = make_set(Message,100), Detail_Message = make_set(DetailedMessage,100), Detail_Data = make_set(DetailedData,100), Total_TransactionId = dcount(TransactionId) by ClientIp, Uri, Action
| where Total_TransactionId >= Threshold
# The Threshold value above can be changed as per your infrastructure's requirement
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Uri
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIp
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let Threshold = 3;
Stage 1: source
AGWFirewallLogs
Stage 2: where
| where Action == "Matched"
Stage 3: where
| where Message has "Injection" or Message has "File Inclusion"
Stage 4: project
| project TransactionId, Hostname, RequestUri, TimeGenerated, ClientIp, Message, DetailedMessage, DetailedData
Stage 5: join
| join kind = inner(
AGWFirewallLogs
| where Action == "Blocked") on TransactionId
Stage 6: extend
extend Uri
Stage 7: summarize
summarize Detail_Data, Detail_Message, EndTime, Message, StartTime, Total_TransactionId, TransactionID by ClientIp, Uri, Action
Stage 8: where
where Total_TransactionId >= 3
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | eq |
| field:"Action" kind:eq |
Message | match |
| field:"Message" kind:match |
Total_TransactionId | ge |
| field:"Total_TransactionId" kind:ge value:"3" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | summarize |
ClientIp | summarize |
Detail_Data | summarize |
Detail_Message | summarize |
EndTime | summarize |
Message | summarize |
StartTime | summarize |
Total_TransactionId | summarize |
TransactionID | summarize |
Uri | summarize |
Armorblox Needs Review Alert
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
'This rule generates an alert for an Armorblox incident where the remediation action is "Needs Review".'
Rule Body
id: 322d4765-be6b-4868-9e3f-138a4f339dd6
name: Armorblox Needs Review Alert
description: |
'This rule generates an alert for an Armorblox incident where the remediation action is "Needs Review".'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Armorblox
dataTypes:
- Armorblox_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
Armorblox_CL
| where remediation_actions_s contains "Needs Review"
| extend users_json = parse_json(users_s)
| extend Name = users_json[0].name, Email = users_json[0].email
| project-away users_json
entityMappings:
- entityType: Mailbox
fieldMappings:
- identifier: MailboxPrimaryAddress
columnName: Email
- identifier: DisplayName
columnName: Name
eventGroupingSettings:
aggregationKind: AlertPerResult
alertDetailsOverride:
alertDisplayNameFormat: Alert from Armorblox
alertDescriptionFormat: 'Incident {{id_s}} generated at {{date_t}} needs review '
alertSeverityColumnName: priority_s
customDetails:
IncidentId: id_s
RemediationAction: remediation_actions_s
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
Armorblox_CL
Stage 2: where
| where remediation_actions_s contains "Needs Review"
Stage 3: extend
| extend users_json = parse_json(users_s)
Stage 4: extend
| extend Name = users_json[0].name, Email = users_json[0].email
Stage 5: project-away
| project-away users_json
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
remediation_actions_s | contains |
| field:"remediation_actions_s" kind:contains value:"Needs Review" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Email | extend |
Name | extend |
AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation
#Identifies creation of IAM policies that grant broad AWS Lambda CRUD permissions and subsequent policy attachment activity to a principal. This behavior may indicate an attempt to add capabilities that support privilege escalation or unauthorized execution pathways.
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 DataPipeline policy and then privilege escalation (Kusto)
Rule Body
id: 22115d3c-e87c-485a-9130-33797d619124
name: AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation
description: |
Identifies creation of IAM policies that grant broad AWS Lambda CRUD permissions and subsequent policy attachment
activity to a principal. This behavior may indicate an attempt to add capabilities that support privilege
escalation or unauthorized execution pathways.
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 "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "lambda:Delete") 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 Lambda 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 "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "lambda:Delete") 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 |
AWSCloudTrail - NRT Login to AWS Management Console without MFA
#Multi-Factor Authentication (MFA) helps you to prevent credential compromise. This alert identifies logins to the AWS Management Console without MFA. You can limit this detection to trigger for administrative accounts if you do not have MFA enabled on all accounts. This is done by looking at the eventName ConsoleLogin and if the AdditionalEventData field indicates MFA was NOT used and the ResponseElements field indicates NOT a Failure. Thereby indicating that a non-MFA login was successful.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth |
Telemetry Coverage
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: 0ee2aafb-4500-4e36-bcb1-e90eec2f0b9b
name: AWSCloudTrail - NRT Login to AWS Management Console without MFA
description: |
'Multi-Factor Authentication (MFA) helps you to prevent credential compromise. This alert identifies logins to the AWS Management Console without MFA.
You can limit this detection to trigger for administrative accounts if you do not have MFA enabled on all accounts.
This is done by looking at the eventName ConsoleLogin and if the AdditionalEventData field indicates MFA was NOT used and the ResponseElements field indicates NOT a Failure. Thereby indicating that a non-MFA login was successful.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
- connectorId: AWSS3
dataTypes:
- AWSCloudTrail
tactics:
- DefenseEvasion
- PrivilegeEscalation
- Persistence
- InitialAccess
relevantTechniques:
- T1078
query: |
AWSCloudTrail
| where EventName =~ "ConsoleLogin"
| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
| where SessionIssuerUserName !contains "AWSReservedSSO"
| 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]), "")
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
version: 1.0.4
kind: NRT
Stages and Predicates
Stage 1: source
AWSCloudTrail
Stage 2: where
| where EventName =~ "ConsoleLogin"
Stage 3: extend
| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
Stage 4: where
| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
Stage 5: where
| where SessionIssuerUserName !contains "AWSReservedSSO"
Stage 6: 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 7: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,
UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventName | eq |
| field:"aws::eventName" kind:eq value:"ConsoleLogin" |
LoginResult | ne |
| field:"LoginResult" kind:ne value:"Failure" |
MFAUsed | ne |
| field:"MFAUsed" kind:ne value:"Yes" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SessionIssuerUserName | contains | AWSReservedSSO | excludes:SessionIssuerUserName field:"SessionIssuerUserName" value:"AWSReservedSSO" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AWSRegion | summarize |
AccountName | summarize |
AccountUPNSuffix | summarize |
EndTimeUtc | summarize |
EventName | summarize |
EventTypeName | summarize |
LoginResult | summarize |
MFAUsed | summarize |
RecipientAccountId | summarize |
SessionMfaAuthenticated | summarize |
SourceIpAddress | summarize |
StartTimeUtc | summarize |
UserAgent | summarize |
UserIdentityAccountId | summarize |
UserIdentityPrincipalid | summarize |
UserIdentityUserName | summarize |
Box - Abmormal user activity
#'Detects spikes (deviations from avarage) in user activity.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Collection |
Rule Body
id: 1139230c-cf10-45db-b616-fed0d1415c05
name: Box - Abmormal user activity
description: |
'Detects spikes (deviations from avarage) in user activity.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoxDataConnector
dataTypes:
- BoxEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Collection
relevantTechniques:
- T1530
query: |
let lbperiod_start = 14d;
let lbperiod_end = 24h;
let user_actions_1 = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| summarize TotalEvents = count() by SourceName
| project TotalEvents, User = SourceName;
let user_actions_2 = BoxEvents
| where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
| summarize TotalEvents = count() by SrcUserName
| project TotalEvents, User = SrcUserName;
let TotalActions = (union user_actions_1, user_actions_2)
| summarize TotalEvents = count() by User
| extend EventsPerDay = TotalEvents / 29
| extend k = 1;
let actions1_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;
let actions2_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;
let TotalActionsLastHour = (union actions1_last_h, actions2_last_h)
| summarize TotalEventsH = sum(TotalEventsH) by User
| extend k = 1;
TotalActions
| join (TotalActionsLastHour) on k
| where EventsPerDay > TotalEventsH
| project User
| extend AccountCustomEntity = User
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Parameters
let lbperiod_start = 14d;
let lbperiod_end = 24h;
Let Binding: actions1_last_h
let actions1_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;
Let Binding: actions2_last_h
let actions2_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;
Let Binding: TotalActionsLastHour
let TotalActionsLastHour = (union actions1_last_h, actions2_last_h)
| summarize TotalEventsH = sum(TotalEventsH) by User
| extend k = 1;
Union (2 Sources)
Each leg below queries one source; the rule matches if any leg does. Sources: user_actions_1, user_actions_2
Leg 1: user_actions_1
Leg 2: user_actions_2
Applied to the Combined Result
| summarize TotalEvents = count() by User
| extend EventsPerDay = TotalEvents / 29
| extend k = 1 | join (TotalActionsLastHour) on k | where EventsPerDay > TotalEventsH | project User | extend AccountCustomEntity = User
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
User | project |
AccountCustomEntity | extend |
Corelight - SMTP Email containing NON Ascii Characters within the Subject
#'Detects where an emails contain non ascii characters within the Subject.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 50c61708-9824-46f3-87cf-22490796fae2
name: Corelight - SMTP Email containing NON Ascii Characters within the Subject
description: |
'Detects where an emails contain non ascii characters within the Subject.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Corelight
dataTypes:
- corelight_smtp
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
corelight_smtp
| where subject hasprefix @'\=?utf-16'
| mv-expand to_recipient = todynamic(to_) to typeof(string)
| summarize recipients = dcount(to_recipient)
| extend k = 1
| join (corelight_smtp
| where subject hasprefix @'\=?utf-16'
| mv-expand to_recipient = todynamic(to_) to typeof(string)
| summarize by to_recipient
| extend k = 1) on k
| where recipients > 1
entityMappings:
- entityType: MailMessage
fieldMappings:
- identifier: Recipient
columnName: to_recipient
version: 2.2.0
kind: Scheduled
Stages and Predicates
Stage 1: source
corelight_smtp
Stage 2: where
| where subject hasprefix @'\=?utf-16'
Stage 3: mv-expand
| mv-expand to_recipient = todynamic(to_) to typeof(string)
Stage 4: summarize
| summarize recipients = dcount(to_recipient)
Stage 5: extend
| extend k = 1
Stage 6: join
| join (corelight_smtp
| where subject hasprefix @'\=?utf-16'
| mv-expand to_recipient = todynamic(to_) to typeof(string)
| summarize by to_recipient
| extend k = 1) on k
Stage 7: where
where recipients > 1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
recipients | gt |
| field:"recipients" kind:gt value:"1" |
subject | starts_with |
| field:"subject" kind:starts_with value:"\=?utf-16" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
recipients | summarize |
k | extend |
Guardian- Secrets Policy Violation Detection
#'This alert creates an incident when Secrets Policy Violation detected from the Guardian.'
Rule Body
id: 1cd8baa8-b8b4-436e-9f3f-4328f52a8e14
name: Guardian- Secrets Policy Violation Detection
description: |
'This alert creates an incident when Secrets Policy Violation detected from the Guardian.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: BoschAIShield
dataTypes:
- Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
Guardian
| where PolicyViolatedControlFeature =~ 'Secrets'
| where Severity =~ 'Medium'
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: NTDomain
columnName: NTDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIP
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: Guardian- Secrets Policy Violation detection
alertDescriptionFormat: |
This query detects Secrets Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
alertTacticsColumnName: null
alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
Guardian
Stage 2: where
| where PolicyViolatedControlFeature =~ 'Secrets'
Stage 3: where
| where Severity =~ 'Medium'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
PolicyViolatedControlFeature | eq |
| field:"PolicyViolatedControlFeature" kind:eq value:"Secrets" |
Severity | eq |
| field:"Severity" kind:eq value:"Medium" |
Mass secret retrieval from Azure Key Vault
#'Identifies mass secret retrieval from Azure Key Vault observed by a single user. Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications. You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 24f8c234-d1ff-40ec-8b73-96b17a3a9c1c
name: Mass secret retrieval from Azure Key Vault
description: |
'Identifies mass secret retrieval from Azure Key Vault observed by a single user.
Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications.
You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: AzureKeyVault
dataTypes:
- KeyVaultData
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1003
query: |
let DistinctSecretsThreshold = 10;
let EventCountThreshold = 50;
// To avoid any False Positives, filtering using AppId is recommended.
// The AppId 509e4652-da8d-478d-a730-e9d4a1996ca4 has been added in the query as it corresponds to Azure Resource Graph performing VaultGet operations for indexing and syncing all tracked resources across Azure.
// The AppId 8cae6e77-e04e-42ce-b5cb-50d82bce26b1 has been added as it correspond to Microsoft Policy Insights Provider Data Plane performing VaultGet operations for policies checks.
let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);
AzureDiagnostics
| where OperationName in (OperationList) and ResourceType =~ "VAULTS"
| where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')
| extend
ResourceId,
ResultType = column_ifexists("ResultType", ""),
identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")
| extend
CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
| as _Retrievals
| where CallerObjectId in (toscalar(
_Retrievals
| where ResultType == "Success"
| summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
| where Count > DistinctSecretsThreshold
| summarize make_set(CallerObjectId,10000)
))
| extend
requestUri_s = column_ifexists("requestUri_s", ""),
id_s = column_ifexists("id_s", ""),
CallerIPAddress = column_ifexists("CallerIPAddress", ""),
clientInfo_s = column_ifexists("clientInfo_s", "")
| summarize
EventCount = count(),
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
ResourceList = make_set(Resource, 50),
OperationNameList = make_set(OperationName, 50),
RequestURLList = make_set(requestUri_s, 50),
ResourceId = max(ResourceId),
CallerIPList = make_set(CallerIPAddress, 50),
clientInfo_sList = make_set(clientInfo_s, 50),
CallerIPMax = max(CallerIPAddress)
by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN
| where EventCount > EventCountThreshold
| project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList
| extend timestamp = EndTime
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: CallerObjectId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIPMax
version: 1.0.8
kind: Scheduled
Stages and Predicates
Parameters
let DistinctSecretsThreshold = 10;
let EventCountThreshold = 50;
let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);
Stage 1: source
AzureDiagnostics
Stage 2: where
| where OperationName in (OperationList) and ResourceType =~ "VAULTS"
Stage 3: where
| where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')
Stage 4: extend
| extend
ResourceId,
ResultType = column_ifexists("ResultType", ""),
identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")
Stage 5: extend
| extend
CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
CallerObjectId =isempty(identity_claim_oid_g)identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_gidentity_claim_oid_gCallerObjectUPN =isempty(identity_claim_upn_s)identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_sidentity_claim_upn_sStage 6: kusto:as
| as _Retrievals
Stage 7: where
| where CallerObjectId in (toscalar(
_Retrievals
| where ResultType == "Success"
| summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
| where Count > DistinctSecretsThreshold
| summarize make_set(CallerObjectId,10000)
))
Stage 8: extend
| extend
requestUri_s = column_ifexists("requestUri_s", ""),
id_s = column_ifexists("id_s", ""),
CallerIPAddress = column_ifexists("CallerIPAddress", ""),
clientInfo_s = column_ifexists("clientInfo_s", "")
Stage 9: summarize
| summarize
EventCount = count(),
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
ResourceList = make_set(Resource, 50),
OperationNameList = make_set(OperationName, 50),
RequestURLList = make_set(requestUri_s, 50),
ResourceId = max(ResourceId),
CallerIPList = make_set(CallerIPAddress, 50),
clientInfo_sList = make_set(clientInfo_s, 50),
CallerIPMax = max(CallerIPAddress)
by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN
Stage 10: where
| where EventCount > EventCountThreshold
Stage 11: project-reorder
| project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList
Stage 12: extend
| extend timestamp = EndTime
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventCount | gt |
| field:"EventCount" kind:gt value:"50" |
OperationName | in |
| field:"OperationName" kind:in |
ResourceType | eq |
| field:"ResourceType" kind:eq value:"VAULTS" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
OperationName | eq | VaultGet | excludes:OperationName field:"OperationName" value:"VaultGet" |
identity_claim_appid_g | in | 509e4652-da8d-478d-a730-e9d4a1996ca4, 8cae6e77-e04e-42ce-b5cb-50d82bce26b1 | excludes:identity_claim_appid_g field:"identity_claim_appid_g" value:"509e4652-da8d-478d-a730-e9d4a1996ca4" field:"identity_claim_appid_g" value:"8cae6e77-e04e-42ce-b5cb-50d82bce26b1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
CallerIPList | summarize |
CallerIPMax | summarize |
CallerObjectId | summarize |
CallerObjectUPN | summarize |
EndTime | summarize |
EventCount | summarize |
OperationNameList | summarize |
RequestURLList | summarize |
ResourceId | summarize |
ResourceList | summarize |
ResourceType | summarize |
ResultType | summarize |
StartTime | summarize |
clientInfo_sList | summarize |
identity_claim_appid_g | summarize |
timestamp | extend |
Objects for Protection Group Changed
#Detects when protection group objects are updated.
Rule Body
id: af97a601-8fac-4628-bdad-5fc0511236b2
name: Objects for Protection Group Changed
description: Detects when protection group objects are 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 == 29140\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n UserName = 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 == 29140
Stage 3: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
UserName = 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:"29140" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
EventId | project |
MessageDetails | project |
Severity | project |
UserName | project |
Palo Alto Prisma Cloud - Access keys are not rotated for 90 days
#'Detects access keys which were not rotated for 90 days.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 777d4993-31bb-4d45-b949-84f58e09fa2f
name: Palo Alto Prisma Cloud - Access keys are not rotated for 90 days
description: |
'Detects access keys which were not rotated for 90 days.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: PaloAltoPrismaCloud
dataTypes:
- PaloAltoPrismaCloud
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1078
query: |
PaloAltoPrismaCloud
| where Reason =~ 'NEW_ALERT'
| where Status =~ 'open'
| where AlertMessage has 'access keys are not rotated for 90 days'
| extend AccountCustomEntity = UserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
PaloAltoPrismaCloud
Stage 2: where
| where Reason =~ 'NEW_ALERT'
Stage 3: where
| where Status =~ 'open'
Stage 4: where
| where AlertMessage has 'access keys are not rotated for 90 days'
Stage 5: extend
| extend AccountCustomEntity = UserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertMessage | match |
| field:"AlertMessage" kind:match value:"access keys are not rotated for 90 days" |
Reason | eq |
| field:"Reason" kind:eq value:"NEW_ALERT" |
Status | eq |
| field:"Status" kind:eq value:"open" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
Pathlock TDnR - SAP Web Dispatcher HTTP Events
#Detects security-relevant events from SAP Web Dispatcher HTTP logs, forwarded by Pathlock Threat Detection and Response. Web Dispatcher anomalies may indicate web application attacks, request smuggling, unauthorized access to backend SAP systems, or exploitation of SAP Fiori and S/4HANA web frontends.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Command & Control |
Rule Body
id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c77
name: Pathlock TDnR - SAP Web Dispatcher HTTP Events
kind: Scheduled
description: >-
Detects security-relevant events from SAP Web Dispatcher HTTP logs, forwarded by Pathlock Threat
Detection and Response. Web Dispatcher anomalies may indicate web application attacks, request
smuggling, unauthorized access to backend SAP systems, or exploitation of SAP Fiori and
S/4HANA web frontends.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: Pathlock_TDnR
dataTypes:
- Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- CommandAndControl
relevantTechniques:
- T1190
- T1071
query: |
Pathlock_TDnR_CL
| where DataSource == "WD_HTTP_LOG"
| 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 == "WD_HTTP_LOG"
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:"WD_HTTP_LOG" |
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 client observed with high reverse DNS lookup count - Static threshold based (ASIM DNS Solution)
#This rule identifies clients with high reverse DNS counts, which could be carrying out reconnaissance or discovery activity. This helps in detecting the possible initial phases of an attack, like discovery and reconnaissance. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 77b7c820-5f60-4779-8bdb-f06e21add5f1
name: Rare client observed with high reverse DNS lookup count - Static threshold based (ASIM DNS Solution)
description: |
'This rule identifies clients with high reverse DNS counts, which could be carrying out reconnaissance or discovery activity. This helps in detecting the possible initial phases of an attack, like discovery and reconnaissance. 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: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Reconnaissance
relevantTechniques:
- T1590
query: |
let threshold = 10;
let stime = 10d;
let etime = 1d;
let SearchDomain = dynamic(["in-addr.arpa"]);
_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
| summarize
StartTimeUtc = min(TimeGenerated),
EndTimeUtc = max(TimeGenerated),
DNSQueryCount=dcount(DnsQuery)
by SrcIpAddr
| where DNSQueryCount > threshold
| project StartTimeUtc, EndTimeUtc, SrcIpAddr, DNSQueryCount
| join kind=leftanti (_Im_Dns(starttime=ago(stime), endtime=ago(etime), domain_has_any=SearchDomain)
| summarize DNSQueryCount=dcount(DnsQuery) by SrcIpAddr, bin(TimeGenerated, 1d)
| where DNSQueryCount > threshold
| project SrcIpAddr, DNSQueryCount
)
on SrcIpAddr
| join kind=inner (_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
| summarize DNSQueries=make_set(DnsQuery, 1000) by SrcIpAddr)
on SrcIpAddr
| extend DNSQuerythreshold = threshold
| project-away SrcIpAddr1
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
eventGroupingSettings:
aggregationKind: AlertPerResult
customDetails:
DNSQueries: DNSQueries
DNSQuerythreshold: DNSQuerythreshold
DNSQueryCount: DNSQueryCount
alertDetailsOverride:
alertDisplayNameFormat: "[Static threshold] Rare client has been observed as making high reverse DNS lookup count - client IP: '{{SrcIpAddr}}'"
alertDescriptionFormat: "Client identified as making high reverse DNS counts which could be carrying out reconnaissance or discovery activity.\n\nReverse DNS lookup threshold is: '{{DNSQuerythreshold}}'\n\nCurrent reverse DNS lookup count from this client is : '{{DNSQueryCount}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
let stime = 10d;
let etime = 1d;
let SearchDomain = dynamic(["in-addr.arpa"]);
Stage 1: source
_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
Stage 2: summarize
| summarize
StartTimeUtc = min(TimeGenerated),
EndTimeUtc = max(TimeGenerated),
DNSQueryCount=dcount(DnsQuery)
by SrcIpAddr
Stage 3: where
| where DNSQueryCount > threshold
Stage 4: project
| project StartTimeUtc, EndTimeUtc, SrcIpAddr, DNSQueryCount
Stage 5: join (negated)
join kind=leftanti (_Im_Dns) on SrcIpAddr
Stage 6: join
| join kind=inner (_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
| summarize DNSQueries=make_set(DnsQuery, 1000) by SrcIpAddr)
on SrcIpAddr
Stage 7: extend
extend DNSQuerythreshold
Stage 8: project-away
project-away SrcIpAddr1
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DNSQueryCount | gt |
| field:"DNSQueryCount" kind:gt value:"10" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DNSQueryCount | gt | 10 | excludes:DNSQueryCount field:"DNSQueryCount" value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DNSQueryCount | project |
EndTimeUtc | project |
SrcIpAddr | project |
StartTimeUtc | project |
DNSQuerythreshold | extend |
Registry Persistence via AppCert DLL Modification
#Adversaries may establish persistence and/or elevate privileges by executing malicious content triggered by AppCert DLLs loaded into processes. Dynamic-link libraries (DLLs) that are specified in the AppCertDLLs Registry key under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\ are loaded into every process that calls the ubiquitously used application programming interface (API) functions CreateProcess, CreateProcessAsUser, CreateProcessWithLoginW, CreateProcessWithTokenW, or WinExec. Ref: https://attack.mitre.org/techniques/T1546/009/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 13: RegistryEvent (Value Set) |
Rule Body
id: c61ad0ac-ad68-4ebb-b41a-74296d3e0044
name: Registry Persistence via AppCert DLL Modification
description: |
'Adversaries may establish persistence and/or elevate privileges by executing malicious content triggered by AppCert DLLs loaded into processes.
Dynamic-link libraries (DLLs) that are specified in the AppCertDLLs Registry key under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\ are loaded into every process that calls the ubiquitously used application programming interface (API) functions CreateProcess, CreateProcessAsUser, CreateProcessWithLoginW, CreateProcessWithTokenW, or WinExec.
Ref: https://attack.mitre.org/techniques/T1546/009/'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1546.009
query: |
Event
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
| parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" *
| where TargetObject has ("\\Control\\Session Manager\\AppCertDLLs\\")
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer, TargetObject, Details
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
- entityType: RegistryKey
fieldMappings:
- identifier: Key
columnName: TargetObject
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
Event
Stage 2: where
| where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
Stage 3: parse
| parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" *
Stage 4: where
| where TargetObject has ("\\Control\\Session Manager\\AppCertDLLs\\")
Stage 5: summarize
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer, TargetObject, Details
Stage 6: extend
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | in |
| field:"EventID" kind:in value:"13" |
EventLog | eq |
| field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational" |
TargetObject | match |
| field:"TargetObject" kind:match value:"\Control\Session Manager\AppCertDLLs\" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
Details | summarize |
EndTimeUtc | summarize |
EventID | summarize |
StartTimeUtc | summarize |
TargetObject | summarize |
DnsDomain | extend |
HostName | extend |
Service Accounts Performing Remote PS
#Service Accounts Performing Remote PowerShell. The purpose behind this detection is for finding service accounts that are performing remote powershell sessions. There are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts. To accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria. One of the main advantages of this method is that only requires server telemetry, and not the attacking client. The first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:. Random_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13. Random_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Telemetry Coverage
Rule Body
id: d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a
name: Service Accounts Performing Remote PS
description: |
Service Accounts Performing Remote PowerShell.
The purpose behind this detection is for finding service accounts that are performing remote powershell sessions.
There are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts.
To accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria.
One of the main advantages of this method is that only requires server telemetry, and not the attacking client.
The first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:.
Random_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13.
Random_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.
severity: High
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceLogonEvents
- DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1210
query: |
let InteractiveTypes = pack_array( // Declare Interactive logon type names
'Interactive',
'CachedInteractive',
'Unlock',
'RemoteInteractive',
'CachedRemoteInteractive',
'CachedUnlock'
);
let WhitelistedCmdlets = pack_array( // List of whitelisted commands that don't provide a lot of value
'prompt',
'Out-Default',
'out-lineoutput',
'format-default',
'Set-StrictMode',
'TabExpansion2'
);
let WhitelistedAccounts = pack_array('FakeWhitelistedAccount'); // List of accounts that are known to perform this activity in the environment and can be ignored
DeviceLogonEvents // Get all logon events...
| where AccountName !in~ (WhitelistedAccounts) // ...where it is not a whitelisted account...
| where ActionType == "LogonSuccess" // ...and the logon was successful...
| where AccountName !contains "$" // ...and not a machine logon.
| where AccountName !has "winrm va_" // WinRM will have pseudo account names that match this if there is an explicit permission for an admin to run the cmdlet, so assume it is good.
| extend IsInteractive=(LogonType in (InteractiveTypes)) // Determine if the logon is interactive (True=1,False=0)...
| summarize HasInteractiveLogon=max(IsInteractive) // ...then bucket and get the maximum interactive value (0 or 1)...
by AccountName // ... by the AccountNames
| where HasInteractiveLogon == 0 // ...and filter out all accounts that had an interactive logon.
// At this point, we have a list of accounts that we believe to be service accounts
// Now we need to find RemotePS sessions that were spawned by those accounts
// Note that we look at all powershell cmdlets executed to form a 29-day baseline to evaluate the data on today
| join kind=rightsemi ( // Start by dropping the account name and only tracking the...
DeviceEvents // ...
| where ActionType == 'PowerShellCommand' // ...PowerShell commands seen...
| where InitiatingProcessFileName =~ 'wsmprovhost.exe' // ...whose parent was wsmprovhost.exe (RemotePS Server)...
| extend AccountName = InitiatingProcessAccountName // ...and add an AccountName field so the join is easier
) on AccountName
// At this point, we have all of the commands that were ran by service accounts
| extend Command = tostring(extractjson('$.Command', tostring(AdditionalFields))) // Extract the actual PowerShell command that was executed
| where Command !in (WhitelistedCmdlets) // Remove any values that match the whitelisted cmdlets
| summarize (Timestamp, ReportId)=arg_max(TimeGenerated, ReportId), // Then group all of the cmdlets and calculate the min/max times of execution...
make_set(Command, 100000), count(), min(TimeGenerated) by // ...as well as creating a list of cmdlets ran and the count..
AccountName, AccountDomain, DeviceName, DeviceId // ...and have the commonality be the account, DeviceName and DeviceId
// At this point, we have machine-account pairs along with the list of commands run as well as the first/last time the commands were ran
| order by AccountName asc // Order the final list by AccountName just to make it easier to go through
| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AccountName
- identifier: DnsDomain
columnName: AccountDomain
- identifier: Name
columnName: AccountName
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');
Let Binding: InteractiveTypes
let InteractiveTypes = pack_array(
'Interactive',
'CachedInteractive',
'Unlock',
'RemoteInteractive',
'CachedRemoteInteractive',
'CachedUnlock'
);
Let Binding: WhitelistedCmdlets
let WhitelistedCmdlets = pack_array(
'prompt',
'Out-Default',
'out-lineoutput',
'format-default',
'Set-StrictMode',
'TabExpansion2'
);
Stage 1: source
DeviceLogonEvents
Stage 2: where
| where AccountName !in~ (WhitelistedAccounts)
Stage 3: where
| where ActionType == "LogonSuccess"
Stage 4: where
| where AccountName !contains "$"
Stage 5: where
| where AccountName !has "winrm va_"
Stage 6: extend
| extend IsInteractive=(LogonType in (InteractiveTypes))
Stage 7: summarize
| summarize HasInteractiveLogon=max(IsInteractive)
by AccountName
Stage 8: where
| where HasInteractiveLogon == 0
Stage 9: join
| join kind=rightsemi (
DeviceEvents
| where ActionType == 'PowerShellCommand'
| where InitiatingProcessFileName =~ 'wsmprovhost.exe'
| extend AccountName = InitiatingProcessAccountName
) on AccountName
Stage 10: extend
extend Command
Stage 11: where
where not (Command == "WhitelistedCmdlets")
Stage 12: summarize
summarize ReportId, Timestamp by AccountName, AccountDomain, DeviceName, DeviceId
Stage 13: sort
sort by AccountName
Stage 14: extend
extend HostName
HostName =DeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))DeviceNameStage 15: extend
extend DnsDomain
DnsDomain =DeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))""Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq |
HasInteractiveLogon | eq |
| field:"HasInteractiveLogon" kind:eq value:"0" |
InitiatingProcessFileName | eq |
| field:"InitiatingProcessFileName" kind:eq value:"wsmprovhost.exe" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AccountName | eq | WhitelistedAccounts | excludes:AccountName field:"AccountName" value:"WhitelistedAccounts" |
AccountName | contains | $ | excludes:AccountName field:"AccountName" value:"$" |
AccountName | match | winrm va_ | excludes:AccountName field:"AccountName" value:"winrm va_" |
Command | eq | WhitelistedCmdlets | excludes:Command field:"Command" value:"WhitelistedCmdlets" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountDomain | summarize |
AccountName | summarize |
DeviceId | summarize |
DeviceName | summarize |
ReportId | summarize |
Timestamp | summarize |
HostName | extend |
DnsDomain | extend |
Tenant State Changed
#Detects when tenant state is updated.
Rule Body
id: 71711b77-486c-41d0-9de0-dcc411fa7b05
name: Tenant State Changed
description: Detects when tenant state 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 == 25000\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 == 25000
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:"25000" |
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 Domain Entity to DeviceNetworkEvents
#This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.
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: 1546f3b3-de8a-4e62-bfea-815422154981
name: TI Map Domain Entity to DeviceNetworkEvents
description: |
'This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.'
severity: Medium
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceNetworkEvents
- 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 dt_lookBack = 1h;
let ioc_lookBack = 14d;
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteUrl)
| where TimeGenerated >= ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
| where isnotempty(Domain)
| project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
let DeviceNetworkEventDomains = DeviceNetworkEvents_
| distinct Domain
| summarize make_list(Domain);
ThreatIntelIndicators
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| where TimeGenerated >= ago(ioc_lookBack)
| extend IndicatorId = tostring(split(Id, "--")[2])
| where DomainName in (DeviceNetworkEventDomains)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil))
| extend Description = tostring(parse_json(Data).description)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
| join kind=innerunique (DeviceNetworkEvents_) on $left.DomainName == $right.Domain
| where DeviceNetworkEvents_TimeGenerated < ValidUntil
| summarize DeviceNetworkEvents_TimeGenerated = arg_max(DeviceNetworkEvents_TimeGenerated, *) by IndicatorId
| project DeviceNetworkEvents_TimeGenerated, IndicatorId, Url = RemoteUrl, Confidence, Description, Tags, TrafficLightProtocolLevel, ActionType, DeviceId, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort
| extend Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
| extend timestamp = DeviceNetworkEvents_TimeGenerated, UserPrincipalName = InitiatingProcessAccountUpn
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: DeviceName
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
- entityType: Process
fieldMappings:
- identifier: CommandLine
columnName: InitiatingProcessCommandLine
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
Let Binding: DeviceNetworkEvents_
let DeviceNetworkEvents_ = DeviceNetworkEvents
| where isnotempty(RemoteUrl)
| where TimeGenerated >= ago(dt_lookBack)
| where ActionType !has "ConnectionFailed"
| extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
| where isnotempty(Domain)
| project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
Let Binding: DeviceNetworkEventDomains
let DeviceNetworkEventDomains = DeviceNetworkEvents_
| distinct Domain
| summarize make_list(Domain);
Stage 1: source
let DeviceNetworkEvents_
Stage 2: source
let DeviceNetworkEventDomains
Stage 3: source
ThreatIntelIndicators
Stage 4: extend
extend IndicatorType
Stage 5: where
where IndicatorType == "domain-name"
Stage 6: extend
extend DomainName
Stage 7: where
where TimeGenerated >= ago(1209600s)
Stage 8: extend
extend IndicatorId
Stage 9: where
where DomainName == "DeviceNetworkEventDomains"
Stage 10: summarize
summarize LatestIndicatorTime by Id, ObservableValue
Stage 11: where
where (isempty(ValidUntil) or ValidUntil > now()) and IsActive == true
Stage 12: extend
extend Description
Stage 13: extend
extend TrafficLightProtocolLevel
Stage 14: where
where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")
Stage 15: project-reorder
project-reorder
Stage 16: join
join kind=innerunique (DeviceNetworkEvents_) on $left.DomainName, $right.Domain
Stage 17: where
where DeviceNetworkEvents_TimeGenerated < ValidUntil
Stage 18: summarize
summarize DeviceNetworkEvents_TimeGenerated by IndicatorId
Stage 19: project
project ActionType, Confidence, Description, DeviceId, DeviceName, DeviceNetworkEvents_TimeGenerated, IndicatorId, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort, Tags, TrafficLightProtocolLevel, Url
Stage 20: extend
extend Name, UPNSuffix
Stage 21: extend
extend UserPrincipalName, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Domain | is_not_null | field:"Domain" kind:is_not_null | |
DomainName | in |
| field:"DomainName" kind:in value:"DeviceNetworkEventDomains" |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
RemoteUrl | is_not_null | field:"RemoteUrl" kind:is_not_null | |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
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 |
|---|---|
ActionType | project |
Confidence | project |
Description | project |
DeviceId | project |
DeviceName | project |
DeviceNetworkEvents_TimeGenerated | project |
IndicatorId | project |
InitiatingProcessAccountUpn | project |
InitiatingProcessCommandLine | project |
RemoteIP | project |
RemotePort | project |
Tags | project |
TrafficLightProtocolLevel | project |
Url | project |
Name | extend |
UPNSuffix | extend |
UserPrincipalName | extend |
timestamp | extend |