Detection rules › Kusto
Kusto rules: 05
API - Suspicious Login
#'42Crunch API protection against suspicious login'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Credential Access |
Rule Body
id: 7bdc10d6-aa24-4ca9-9a93-802cd8761354
name: API - Suspicious Login
description: |
'42Crunch API protection against suspicious login'
severity: High
requiredDataConnectors:
- connectorId: 42CrunchAPIProtection
dataTypes:
- FortyTwoCrunchAPIProtection
- connectorId: FortyTwoCrunchAPIProtection
dataTypes:
- FortyTwoCrunchAPIProtection
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: SingleAlert
status: Available
tactics:
- CredentialAccess
- InitialAccess
relevantTechniques:
- T1110
- T1190
query: |
let firstRec = FortyTwoCrunchAPIProtection
| where TimeGenerated >= ago(5m)
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
| where UriPath has "/api/login?user=" and Status in (200, 403);
let ipAddressCount = toscalar(firstRec | summarize by SourceIp | count);
let secondRec = firstRec | summarize arg_max(TimeGenerated, *) by SourceIp;
let recCount = iff((toscalar(secondRec | count) > 3), ipAddressCount, 0);
secondRec | take recCount
customDetails:
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIp
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: Hostname
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InstanceName
version: 3.0.1
kind: Scheduled
Stages and Predicates
let firstRec and let secondRec are inlined into the numbered stages below.
Let Binding: ipAddressCount
let ipAddressCount = toscalar(firstRec | summarize by SourceIp | count);
Let Binding: recCount
let recCount = iff((toscalar(secondRec | count) > 3), ipAddressCount, 0);
Stages 1 to 5 define let secondRec as the rule's main pipeline source. Stage 6 uses it.
Stage 1: source
FortyTwoCrunchAPIProtection
Stage 2: where
| where TimeGenerated >= ago(5m)
Stage 3: project-away
| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
Stage 4: where
| where UriPath has "/api/login?user=" and Status in (200, 403)
Stage 5: summarize
| summarize arg_max(TimeGenerated, *) by SourceIp
Stage 6: take
secondRec
| take recCount
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Status | in |
| field:"Status" kind:in |
UriPath | match |
| field:"UriPath" kind:match value:"/api/login?user=" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SourceIp | summarize |
Conditional Access - A new Conditional Access policy was created
#A new Conditional Access policy was created in Entra ID.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add conditional access policy |
Rules Detecting the Same Action
These rules filter on the same operation.
- Azure MFA Disabled (Panther)
- Azure Policy Violation Detected (Panther)
- CA Policy Removed by Non Approved Actor (Sigma)
- CA Policy Updated by Non Approved Actor (Sigma)
- Certificate-Based Authentication Enabled (Sigma)
- Changes to Device Registration Policy (Sigma)
- Conditional Access - A Conditional Access app exclusion has changed (Kusto)
- Conditional Access - A Conditional Access Device platforms condition has changed (the Device platforms condition can be spoofed) (Kusto)
Rule Body
id: 0459a1b5-909d-4783-9e27-24536b05a47f
name: Conditional Access - A new Conditional Access policy was created
version: 1.0.1
kind: Scheduled
description: A new Conditional Access policy was created in Entra ID.
severity: Informational
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562.007
query: |+
// A new Conditional Access policy was created.
AuditLogs
| where OperationName in ("Add conditional access policy")
| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])
| project
TimeGenerated,
OperationName,
policy = TargetResources[0].displayName,
modifiedBy,
accountName,
upnSuffix,
result = Result,
newPolicy = TargetResources[0].modifiedProperties[0].newValue
| order by TimeGenerated desc
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: accountName
- identifier: UPNSuffix
columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: AlertPerResult
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT1H
matchingMethod: AllEntities
groupByEntities: []
groupByAlertDetails: []
groupByCustomDetails: []
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName in ("Add conditional access policy")
Stage 3: extend (3 consecutive steps)
| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])
Stage 4: project
| project
TimeGenerated,
OperationName,
policy = TargetResources[0].displayName,
modifiedBy,
accountName,
upnSuffix,
result = Result,
newPolicy = TargetResources[0].modifiedProperties[0].newValue
Stage 5: sort
| order by TimeGenerated desc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | in |
| field:"OperationName" kind:in value:"Add conditional access policy" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
OperationName | project |
TimeGenerated | project |
accountName | project |
modifiedBy | project |
newPolicy | project |
policy | project |
result | project |
upnSuffix | project |
CYFIRMA - Brand Intelligence - Product/Solution Medium Rule
#This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.
"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. The IP has been previously associated with hacking activity and web application attacks. Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Resource Development | |
| Initial Access |
Rule Body
id: 458d964f-d039-4ce0-9741-0b6245ba3374
name: CYFIRMA - Brand Intelligence - Product/Solution Medium Rule
description: |
"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure.
The IP has been previously associated with hacking activity and web application attacks.
Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
- connectorId: CyfirmaBrandIntelligenceAlertsDC
dataTypes:
- CyfirmaBIProductSolutionAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- ResourceDevelopment
- InitialAccess
relevantTechniques:
- T1585.002
- T1583.001
- T1566
- T1583
query: |
// Medium severity - Product/Solution Impersonation
let timeFrame = 5m;
CyfirmaBIProductSolutionAlerts_CL
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=asset_value,
Impact=impact,
Recommendation=recommendation,
SourceSype=source_type,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
Recommendation,
SourceSype,
ProductName,
ProviderName
customDetails:
AssetType: AssetType
AssetValue: AssetValue
Impact: Impact
Recommendation: Recommendation
SourceSype: SourceSype
Description: Description
RiskScore: RiskScore
TimeGenerated: TimeGenerated
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - Brand Impersonation Detected for Product/Solution - Medium Severity - {{AssetType}} : {{AssetValue}} "
alertDescriptionFormat: "{{Description}} "
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaBIProductSolutionAlerts_CL
Stage 2: where
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
AlertUID=alert_uid,
UID=uid,
AssetType=asset_type,
AssetValue=asset_value,
Impact=impact,
Recommendation=recommendation,
SourceSype=source_type,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Description,
RiskScore,
FirstSeen,
LastSeen,
AlertUID,
UID,
AssetType,
AssetValue,
Impact,
Recommendation,
SourceSype,
ProductName,
ProviderName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
AssetType | project |
AssetValue | project |
Description | project |
FirstSeen | project |
Impact | project |
LastSeen | project |
ProductName | project |
ProviderName | project |
Recommendation | project |
RiskScore | project |
SourceSype | project |
TimeGenerated | project |
UID | project |
DumpGuard NTLM challenge detected
#With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. The creator of the DumpGuard tool purposely used a hard-coded NTLMv1 challenge into the tool, for easy detection. > [!WARNING] > Since the detection relies on a static IOC that can easily be changed in the source code, this detection has a low confidence score since it can be easily bypassed. However, if the detection hits it is almost 100% certain the alert will be TP. > Also take into account that the NetworkSignatureInspected ActionType in MDE is sampled, which means not very event will be logged.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
References
Telemetry Coverage
Rule Body
DeviceNetworkEvents
// Get NTLM Challenges
| where ActionType == "NetworkSignatureInspected"
| where tostring(todynamic(AdditionalFields).SignatureName) =~ "NTLM-Challenge"
// Extract the NTLM Sample Packet
| extend SamplePacketContent = extract('\\["(.+)"\\]', 1, tostring(todynamic(AdditionalFields).SamplePacketContent))
// Remove % values, since the '1122334455667788' is easy to find without conversions
| extend NewSamplePacketContent = strcat_array(split(SamplePacketContent, "%"), "")
| where NewSamplePacketContent contains "1122334455667788"
Stages and Predicates
Stage 1: source
DeviceNetworkEvents
Stage 2: where
| where ActionType == "NetworkSignatureInspected"
Stage 3: where
| where tostring(todynamic(AdditionalFields).SignatureName) =~ "NTLM-Challenge"
Stage 4: extend
| extend SamplePacketContent = extract('\\["(.+)"\\]', 1, tostring(todynamic(AdditionalFields).SamplePacketContent))
Stage 5: extend
| extend NewSamplePacketContent = strcat_array(split(SamplePacketContent, "%"), "")
Stage 6: where
| where NewSamplePacketContent contains "1122334455667788"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ActionType | eq |
| field:"ActionType" kind:eq value:"NetworkSignatureInspected" |
AdditionalFields.SignatureName | eq |
| field:"AdditionalFields.SignatureName" kind:eq value:"NTLM-Challenge" |
NewSamplePacketContent | contains |
| field:"NewSamplePacketContent" kind:contains value:"1122334455667788" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SamplePacketContent | extend |
NewSamplePacketContent | extend |
LDAP reconnaissance via search filters
#This query collects LDAP queries executed on systems monitored by Defender for Endpoint. The filter and distinguished name attributes of these queries are extracted and matched against a list of IOCs obtained from popular reconnaissance tools.
Known False Positives
- Some legitimate administration tools will also perform LDAP queries that are identified as reconnaissance.
MITRE ATT&CK Coverage
References
Telemetry Coverage
Rule Body
let timeframe = 2*1h;
let suspect_search_filter=dynamic([
"(&(&(objectCategory=person)(objectClass=user))(|(description=*pass*)(comment=*pass*)))", // Metasploit IOC [1].
"(&(objectCategory=computer)(operatingSystem=*server*))", // Metasploit IOC [1].
"(&(objectClass=group))", // Metasploit IOC [1].
"(&(objectClass=group)(managedBy=*)),(&(objectClass=group)(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))", // Metasploit IOC [1].
"(&(sAMAccountType=805306369)(dnshostname=*))", // Powerview IOC [1].
"(&(samAccountType=805306368)(samAccountName=*)", // Powerview IOC [1].
"(&(samAccountType=805306368)(servicePrincipalName=*)", // Powerview IOC [1].
"(&(objectClass=msDFS-Linkv2))", // Powerview IOC [1].
"(&(objectCategory =organizationalUnit)(name=*))" // Powerview IOC [1].
// "(samAccountType=805306368)" // Empire IOC [1] - disabled by default, since it is too generic.
"objectClass=trustedDomain", // Recon IOC [2].
// "objectClass=crossRef", // Recon IOC [2].
"userAccountControl:1.2.840.113556.1.4.803:=524288", // Recon IOC [2].
"userAccountControl:1.2.840.113556.1.4.803:=4194304", // AD Recon IOC [2].
"anr=Remote Desktop Users", // AD Recon IOC [2].
"defender-tokenData=*", // AD Recon IOC [2].
"Domain Admins", // AD Recon IOC, based on own research.
"Enterprise Admins", // AD Recon IOC, based on own research.
"CN=Administrators", // AD Recon IOC, based on own research.
"(schemaIDGUID=*)", // Sharphound IOC, based on own research.
// "(objectclass=domain)", // Sharphound IOC, based on own research.
"(&(objectclass=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))" // Sharphound IOC, based on own research.
"(|(samAccountType=805306368)(samAccountType=805306369)(objectclass=organizationalUnit))", // Sharphound IOC, based on own research.
"(samAccountType=805306368)(samAccountType=805306369)" // Sharphound IOC, based on own research.
]);
let suspect_dn=dynamic([
"CN=DnsAdmins", // AD Recon IOC, based on own research.
"CN=Domain Controllers", // Sharphound IOC, based on own research.
"CN=DnsAdmins", // Sharphound IOC, based on own research.
"CN=Read-only Domain Controllers", // Sharphound IOC, based on own research.
"CN=Cloneable Domain Controllers" // Sharphound IOC, based on own research.
]);
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType =~ "LdapSearch"
// Begin environment-specific filter.
// End environment-specific filter.
| where not(InitiatingProcessFolderPath startswith @"c:\program files\azure advanced threat protection sensor\" and InitiatingProcessVersionInfoOriginalFileName == @"Microsoft.Tri.Sensor.exe")
| extend AdditionalFields=parse_json(AdditionalFields)
| extend SearchFilter=tostring(AdditionalFields.SearchFilter)
| extend DistinguishedName=tostring(AdditionalFields.DistinguishedName)
| extend AttributeList=tostring(AdditionalFields.AttributeList)
| where SearchFilter has_any(suspect_search_filter) or DistinguishedName has_any(suspect_dn)
| project-reorder DeviceName, InitiatingProcessFolderPath, SearchFilter, DistinguishedName, InitiatingProcessCommandLine
// Begin environment-specific filter.
// End environment-specific filter.
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and InitiatingProcessFileName =~ "microsoft.tri.sensor.exe")
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and SoftwareName =~ "Endpoint Configuration Manager")
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and SoftwareName contains "AD Connect")
| where not(Signer =~ "Microsoft Windows" and IsCertificateValid == 1 and InitiatingProcessFileName =~ "Microsoft.IdentityServer.ServiceHost.exe")
| where not(InitiatingProcessFolderPath =~ @"c:\windows\system32\lsass.exe")
// Begin environment-specific filter.
// End environment-specific filter.
Ngrok Reverse Proxy on Network (ASIM DNS Solution)
#This detection identifies the top four Ngrok domains from DNS resolution. Ngrok reverse proxy can bypass network defense. While not inherently harmful, it has been used for malicious activities recently.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 22: DNSEvent (DNS query) |
Rule Body
id: 50b0dfb7-2c94-4eaf-a332-a5936d78c263
name: Ngrok Reverse Proxy on Network (ASIM DNS Solution)
description: |
'This detection identifies the top four Ngrok domains from DNS resolution. Ngrok reverse proxy can bypass network defense. While not inherently harmful, it has been used for malicious activities recently.'
severity: Medium
status: Available
tags:
- Schema: ASimDns
SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1572
- T1090
- T1102
query: |
// Define a list of Ngrok domains
let NgrokDomains = dynamic(["ngrok.com", "ngrok.io", "ngrok", "tunnel.com", "korgn", "lennut.com"]);
// Query the _Im_Dns function for the past 1 hour
_Im_Dns(starttime=ago(1h))
| where isnotempty(DnsQuery) // Filter out empty DNS queries
| where DnsQuery has_any (NgrokDomains) // Filter DNS queries that match any of the Ngrok domains
| summarize Starttime = min(EventStartTime),Endtime=max(EventEndTime),EventsCount=sum(EventCount),EventResults=make_set(EventResult,4) by DnsQuery, Domain, SrcIpAddr, Dvc
// Summarize the data by Domain, DNS query, source IP address, and device Dvc
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
eventGroupingSettings:
aggregationKind: AlertPerResult
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let NgrokDomains = dynamic(["ngrok.com", "ngrok.io", "ngrok", "tunnel.com", "korgn", "lennut.com"]);
Stage 1: source
_Im_Dns(starttime=ago(1h))
Stage 2: where
| where isnotempty(DnsQuery)
Stage 3: where
| where DnsQuery has_any (NgrokDomains)
Stage 4: summarize
| summarize Starttime = min(EventStartTime),Endtime=max(EventEndTime),EventsCount=sum(EventCount),EventResults=make_set(EventResult,4) by DnsQuery, Domain, SrcIpAddr, Dvc
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DnsQuery | is_not_null | field:"QueryName" kind:is_not_null | |
DnsQuery | match |
| field:"QueryName" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsQuery | summarize |
Domain | summarize |
Dvc | summarize |
Endtime | summarize |
EventResults | summarize |
EventsCount | summarize |
SrcIpAddr | summarize |
Starttime | summarize |
Port Scan Detected
#'This alert creates an incident when a source IP addresses attempt to communicate with a large amount of distinct ports within a short period.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Discovery |
Rule Body
id: 427e4c9e-8cf4-4094-a684-a2d060dbca38
name: Port Scan Detected
description: |
'This alert creates an incident when a source IP addresses attempt to communicate with a large amount of distinct ports within a short period.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SyslogAma
datatypes:
- Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1046
query: |
let threshold = 50;
SophosXGFirewall
| where Log_Type =~ "Firewall"
| where not(ipv4_is_match("10.0.0.0",Src_IP,8) or ipv4_is_match("172.16.0.0",Src_IP,12) or ipv4_is_match("192.168.0.0",Src_IP,16))
| summarize dcount(Dst_Port) by Src_IP, bin(TimeGenerated, 5m)
| where dcount_Dst_Port > threshold
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Src_IP
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 50;
Stage 1: source
SophosXGFirewall
Stage 2: where
| where Log_Type =~ "Firewall"
Stage 3: where
| where not(ipv4_is_match("10.0.0.0",Src_IP,8) or ipv4_is_match("172.16.0.0",Src_IP,12) or ipv4_is_match("192.168.0.0",Src_IP,16))
Stage 4: summarize
| summarize dcount(Dst_Port) by Src_IP, bin(TimeGenerated, 5m)
Stage 5: where
| where dcount_Dst_Port > threshold
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Log_Type | eq |
| field:"Log_Type" kind:eq value:"Firewall" |
dcount_Dst_Port | gt |
| field:"dcount_Dst_Port" kind:gt value:"50" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Src_IP | cidr_match | 10.0.0.0/8 | excludes:Src_IP field:"Src_IP" value:"10.0.0.0/8" |
Src_IP | cidr_match | 172.16.0.0/12 | excludes:Src_IP field:"Src_IP" value:"172.16.0.0/12" |
Src_IP | cidr_match | 192.168.0.0/16 | excludes:Src_IP field:"Src_IP" value:"192.168.0.0/16" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Src_IP | summarize |
Potential Remote Desktop Tunneling
#This query detects remote desktop authentication attempts with a localhost source address, which can indicate a tunneled login. Ref: https://www.mandiant.com/resources/bypassing-network-restrictions-through-rdp-tunneling
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
Rule Body
id: d2e8fd50-8d66-11ec-b909-0242ac120002
name: Potential Remote Desktop Tunneling
description: |
'This query detects remote desktop authentication attempts with a localhost source address, which can indicate a tunneled login.
Ref: https://www.mandiant.com/resources/bypassing-network-restrictions-through-rdp-tunneling'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CommandAndControl
relevantTechniques:
- T1572
query: |
SecurityEvent
| where EventID in (4624,4625) and LogonType in (10) and IpAddress in ("::1","127.0.0.1")
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, TargetUserName, TargetLogonId, LogonType, IpAddress
| extend Name=tostring(split(TargetUserName, "@")[0]), UPNSuffix=tostring(split(TargetUserName, "@")[1])
| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IpAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Stage 1: source
SecurityEvent
Stage 2: where
| where EventID in (4624,4625) and LogonType in (10) and IpAddress in ("::1","127.0.0.1")
Stage 3: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, TargetUserName, TargetLogonId, LogonType, IpAddress
Stage 4: extend
| extend Name=tostring(split(TargetUserName, "@")[0]), UPNSuffix=tostring(split(TargetUserName, "@")[1])
Stage 5: 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 |
IpAddress | in |
| field:"SourceIp" kind:in |
LogonType | in |
| field:"LogonType" kind:in value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Computer | summarize |
EndTime | summarize |
EventID | summarize |
IpAddress | summarize |
LogonType | summarize |
StartTime | summarize |
TargetLogonId | summarize |
TargetUserName | summarize |
Name | extend |
UPNSuffix | extend |
DnsDomain | extend |
HostName | extend |
Process Execution Frequency Anomaly
#This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors. The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns. Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity. Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Execution |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Security-Auditing | Event ID 4688: A new process has been created. |
Rule Body
id: 2c55fe7a-b06f-4029-a5b9-c54a2320d7b8
name: Process Execution Frequency Anomaly
description: |
'This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors.
The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns.
Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity.
Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.'
severity: Medium
requiredDataConnectors:
- connectorId: SecurityEvents
dataTypes:
- SecurityEvent
- connectorId: WindowsSecurityEvents
dataTypes:
- SecurityEvent
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- Execution
relevantTechniques:
- T1059
query: |
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 5;
// Configure the list with sensitive process names
let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);
let TimeSeriesData =
SecurityEvent
| where EventID == 4688 | extend Process = tolower(Process)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| where Process in~ (ExeList)
| project TimeGenerated, Computer, AccountType, Account, Process
| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Process;
let TimeSeriesAlerts = materialize(TimeSeriesData
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 1.5, -1, 'linefit')
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
| where anomalies > 0
| project Process, TimeGenerated, Total, baseline, anomalies, score
| where Total > TotalEventsThreshold);
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join (
SecurityEvent
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| where EventID == 4688 | extend Process = tolower(Process)
| summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
) on Process, TimeGenerated
| project AnomalyHour = TimeGenerated, Computer, Account, Process, CommandLine, CommandlineCount, Total, baseline, anomalies, score
| extend timestamp = AnomalyHour, NTDomain = split(Account, '\\', 0)[0], Name = split(Account, '\\', 1)[0], HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Account
- identifier: Name
columnName: Name
- identifier: NTDomain
columnName: NTDomain
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.6
kind: Scheduled
Stages and Predicates
Parameters
let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 5;
let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);
let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.
Let Binding: AnomalyHours
let AnomalyHours = materialize(TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated);
Stages 1 to 15 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 16 to 19 use it.
Stage 1: source
let TimeSeriesData
Stage 2: source
let TimeSeriesAlerts
Stage 3: source
let AnomalyHours
Stage 4: source
SecurityEvent
Stage 5: where
| where EventID == 4688
Stage 6: extend
| extend Process = tolower(Process)
Stage 7: where
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
Stage 8: where
| where Process in~ (ExeList)
Stage 9: project
| project TimeGenerated, Computer, AccountType, Account, Process
The stages below score time-series anomalies (make-series, series_decompose_anomalies).
Stage 10: make-series
| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Process
Stage 11: extend
| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 1.5, -1, 'linefit')
Stage 12: mv-expand
| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
Stage 13: where
| where anomalies > 0
Stage 14: project
| project Process, TimeGenerated, Total, baseline, anomalies, score
Stage 15: where
| where Total > TotalEventsThreshold
Stage 16: where
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
Stage 17: join
| join (
SecurityEvent
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| where EventID == 4688 | extend Process = tolower(Process)
| summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
) on Process, TimeGenerated
Stage 18: project
project Account, AnomalyHour, CommandLine, CommandlineCount, Computer, Process, Total, anomalies, baseline, score
Stage 19: extend
extend DnsDomain, HostName, NTDomain, Name, timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventID | eq |
| field:"EventID" kind:eq value:"4688" |
Process | in |
| field:"Process" kind:in |
Total | gt |
| field:"Total" kind:gt value:"5" |
anomalies | gt |
| field:"anomalies" kind:gt value:"0" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Account | project |
AnomalyHour | project |
CommandLine | project |
CommandlineCount | project |
Computer | project |
Process | project |
Total | project |
anomalies | project |
baseline | project |
score | project |
DnsDomain | extend |
HostName | extend |
NTDomain | extend |
Name | extend |
timestamp | extend |
Rare Process as a Service
#This query is looking for rarely seen processes which are launched as a service. Whiltelisted process list need to be updated based on the environment. Author: Jouni Mikkola More info: https://threathunt.blog/rare-process-launch-as-a-service/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
Rule Body
id: 91a451e3-178f-41b2-9e5d-da97d75b9971
name: Rare Process as a Service
description: |
This query is looking for rarely seen processes which are launched as a service. Whiltelisted process list need to be updated based on the environment.
Author: Jouni Mikkola
More info: https://threathunt.blog/rare-process-launch-as-a-service/
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: MicrosoftThreatProtection
dataTypes:
- DeviceProcessEvents
- DeviceNetworkEvents
- DeviceFileEvents
- DeviceImageLoadEvents
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
relevantTechniques:
- T1543
- T1543.003
query: |
let LookupTime = 14d;
let WhiteList = pack_array(
"svchost.exe",
"mssense.exe",
"msmpeng.exe",
"searchindexer.exe",
"microsoftedgeupdate.exe"
);
let GetServices = materialize (
DeviceProcessEvents
| where TimeGenerated > ago(LookupTime)
| where InitiatingProcessParentFileName contains "services.exe"
| where InitiatingProcessFileName !in~(WhiteList)
| project TimeGenerated, DeviceName, StartedChildProcess = FileName, StartedChildProcessSHA1 = SHA1, StartedChildProcessCmdline = ProcessCommandLine, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName
);
GetServices
| summarize count() by ServiceProcess, DeviceName
| where count_ < 6
| join kind = inner GetServices on ServiceProcess, DeviceName
| join kind = leftouter (
DeviceNetworkEvents
| where TimeGenerated > ago(LookupTime)
| where InitiatingProcessParentFileName contains "services.exe"
| where InitiatingProcessFileName !in~(WhiteList)
| project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, NetworkAction = ActionType, RemoteIP, RemoteUrl
) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
| join kind = leftouter (
DeviceFileEvents
| where TimeGenerated > ago(LookupTime)
| where InitiatingProcessParentFileName contains "services.exe"
| where InitiatingProcessFileName !in~(WhiteList)
| project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, FileAction = ActionType, ModifiedFile = FileName, ModifiedFileSHA1 = SHA1, ModifiedFilePath = FolderPath
) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
| join kind = leftouter (
DeviceImageLoadEvents
| where TimeGenerated > ago(LookupTime)
| where InitiatingProcessParentFileName contains "services.exe"
| where InitiatingProcessFileName !in~(WhiteList)
| project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, LoadedDLL = FileName, LoadedDLLSHA1 = SHA1, LoadedDLLPath = FolderPath
) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
| summarize ConnectedAddresses = make_set(RemoteIP, 100000), ConnectedUrls = make_set(RemoteUrl, 100000), FilesModified = make_set(ModifiedFile, 100000),FileModFolderPath = make_set(ModifiedFilePath, 100000),FileModHA1s = make_set(ModifiedFileSHA1, 100000), ChildProcesses = make_set(StartedChildProcess, 100000), ChildCommandlines = make_set(StartedChildProcessCmdline, 100000), DLLsLoaded = make_set(LoadedDLL, 100000), DLLSHA1 = make_set(LoadedDLLSHA1, 100000) by DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
| 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: Process
fieldMappings:
- identifier: ProcessId
columnName: ServiceProcessID
- identifier: CommandLine
columnName: ServiceProcessCmdline
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let LookupTime = 14d;
let GetServices is inlined into the numbered stages below.
Let Binding: WhiteList
let WhiteList = pack_array(
"svchost.exe",
"mssense.exe",
"msmpeng.exe",
"searchindexer.exe",
"microsoftedgeupdate.exe"
);
Stage 1: source
let GetServices
Stage 2: source
DeviceProcessEvents
Stage 3: where
where TimeGenerated > ago(1209600s)
Stage 4: where
where InitiatingProcessParentFileName contains "services.exe"
Stage 5: where
where not (InitiatingProcessFileName =~ "WhiteList")
Stage 6: project
project DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessSHA1, ServiceProcessUser, StartedChildProcess, StartedChildProcessCmdline, StartedChildProcessSHA1, TimeGenerated
Stage 7: summarize
summarize by ServiceProcess, DeviceName
Stage 8: where
where count_ < 6
Stage 9: join
join kind=inner (GetServices) on ServiceProcess, DeviceName
Stage 10: join
join kind=leftouter (DeviceNetworkEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
Stage 11: join
join kind=leftouter (DeviceFileEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
Stage 12: join
join kind=leftouter (DeviceImageLoadEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
Stage 13: summarize
summarize ChildCommandlines, ChildProcesses, ConnectedAddresses, ConnectedUrls, DLLSHA1, DLLsLoaded, FileModFolderPath, FileModHA1s, FilesModified by DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
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 |
|---|---|---|---|
InitiatingProcessParentFileName | contains |
| field:"InitiatingProcessParentFileName" kind:contains value:"services.exe" |
count_ | lt |
| field:"count_" kind:lt value:"6" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
InitiatingProcessFileName | eq | WhiteList | excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"WhiteList" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ChildCommandlines | summarize |
ChildProcesses | summarize |
ConnectedAddresses | summarize |
ConnectedUrls | summarize |
DLLSHA1 | summarize |
DLLsLoaded | summarize |
DeviceName | summarize |
FileModFolderPath | summarize |
FileModHA1s | summarize |
FilesModified | summarize |
ServiceProcess | summarize |
ServiceProcessCmdline | summarize |
ServiceProcessCreationTime | summarize |
ServiceProcessID | summarize |
ServiceProcessSHA1 | summarize |
ServiceProcessUser | summarize |
HostName | extend |
DnsDomain | extend |
RSA ID Plus - Locked Administrator Account Detected
#'Raises an alert when an admin account is locked out of the Admin console (RSAIDPlus Admin Events).'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access | No specific technique |
| Impact | No specific technique |
Rule Body
id: "488c759d-a82e-44cd-91bb-d766573918d7"
name: "RSA ID Plus - Locked Administrator Account Detected"
description: |
'Raises an alert when an admin account is locked out of the Admin console (RSAIDPlus Admin Events).'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: RSAIDPlus_AdminLogs_Connector
dataTypes:
- RSAIDPlus_AdminLogs_CL
queryFrequency: 30m
queryPeriod: 1d
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
- Impact
- CredentialAccess
relevantTechniques: []
query: |
RSAIDPlus_AdminLogs_CL
|where activityKey == 'LOCKED_ADMIN_ACCOUNT'
|project TimeGenerated, customerName, adminUserName, adminUserRole, activityKey, sourceIPAddress
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: sourceIPAddress
- entityType: Account
fieldMappings:
- identifier: Name
columnName: adminUserName
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: customerName
incidentConfiguration:
createIncident: true
groupingConfiguration :
enabled: true
reopenClosedIncident: false
lookbackDuration : 2h
matchingMethod : AllEntities
eventGroupingSettings:
aggregationKind: SingleAlert
alertDetailsOverride:
alertDisplayNameFormat: 'RSA ID Plus Administrator - {{{adminUserName}} is locked out.'
alertDescriptionFormat: 'Administrator - {{{adminUserName}} with Role - {{{adminUserRole}} got locked out of Admin Console at: {{{TimeGenerated}}'
customDetails:
Customer_Name: customerName
Administrator: adminUserName
Administrator_Type: adminUserRole
Activity: activityKey
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
RSAIDPlus_AdminLogs_CL
Stage 2: where
| where activityKey == 'LOCKED_ADMIN_ACCOUNT'
Stage 3: project
| project TimeGenerated, customerName, adminUserName, adminUserRole, activityKey, sourceIPAddress
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
activityKey | eq |
| field:"activityKey" kind:eq value:"LOCKED_ADMIN_ACCOUNT" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
TimeGenerated | project |
activityKey | project |
adminUserName | project |
adminUserRole | project |
customerName | project |
sourceIPAddress | project |
Tailscale: Mass credential revocation in short window
#Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Stealth |
Rule Body
id: f817e2fa-6fa0-fc25-5369-cef9b58771af
name: "Tailscale: Mass credential revocation in short window"
description: |
Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise.
severity: High
status: Available
requiredDataConnectors:
- connectorId: TailscaleCCF
dataTypes:
- Tailscale_Audit_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Impact
relevantTechniques:
- T1070
query: |
Tailscale_Audit_CL
| where Action in ("REVOKE", "DELETE")
| where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT", "AUTH_KEY")
| extend ActorLogin = tostring(Actor.loginName)
| summarize
RevokedCount = count(),
TargetTypes = make_set(tostring(Target.type)),
TargetIds = make_set(tostring(Target.id)),
FirstEvent = min(TimeGenerated),
LastEvent = max(TimeGenerated)
by ActorLogin, bin(TimeGenerated, 1h)
| where RevokedCount >= 5
| project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorLogin
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0
Stages and Predicates
Stage 1: source
Tailscale_Audit_CL
Stage 2: where
| where Action in ("REVOKE", "DELETE")
Stage 3: where
| where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT", "AUTH_KEY")
Stage 4: extend
| extend ActorLogin = tostring(Actor.loginName)
Stage 5: summarize
| summarize
RevokedCount = count(),
TargetTypes = make_set(tostring(Target.type)),
TargetIds = make_set(tostring(Target.id)),
FirstEvent = min(TimeGenerated),
LastEvent = max(TimeGenerated)
by ActorLogin, bin(TimeGenerated, 1h)
Stage 6: where
| where RevokedCount >= 5
Stage 7: project
| project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | in |
| field:"Action" kind:in |
RevokedCount | ge |
| field:"RevokedCount" kind:ge value:"5" |
Target.type | in |
| field:"Target.type" kind:in |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorLogin | project |
FirstEvent | project |
LastEvent | project |
RevokedCount | project |
TargetIds | project |
TargetTypes | project |
TimeGenerated | project |
Trend Micro CAS - Ransomware infection
#'Triggeres when ransomware was detected.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
Rule Body
id: 0bec3f9a-dbe9-4b4c-9ff6-498d64bbef90
name: Trend Micro CAS - Ransomware infection
description: |
'Triggeres when ransomware was detected.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: TrendMicroCAS
dataTypes:
- TrendMicroCAS
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1486
query: |
TrendMicroCAS
| where EventType =~ 'ransomware'
| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = RansomwareName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: MalwareCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
TrendMicroCAS
Stage 2: where
| where EventType =~ 'ransomware'
Stage 3: extend
| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = RansomwareName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventType | eq |
| field:"EventType" kind:eq value:"ransomware" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
MalwareCustomEntity | extend |