Detection rules › Kusto

Kusto rules: 1k

Apache - Multiple server errors from single IP

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

'Detects multiple server errors from one source in short timeframe'

MITRE ATT&CK Coverage

Rule Body

id: 1bf246a2-3af9-11ec-8d3d-0242ac130003
name: Apache - Multiple server errors from single IP
description: |
  'Detects multiple server errors from one source in short timeframe'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ApacheHTTPServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - InitialAccess
relevantTechniques:
  - T1498
  - T1190
  - T1133
query: |
  let threshold = 100;
  ApacheHTTPServer
  | where HttpStatusCode >= 500 and HttpStatusCode <= 599 
  | summarize MultipleServerErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)
  | where MultipleServerErrors > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

ApacheHTTPServer

Stage 2: where

| where HttpStatusCode >= 500 and HttpStatusCode <= 599

Stage 3: summarize

| summarize MultipleServerErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 4: where

| where MultipleServerErrors > threshold

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MultipleServerErrorssummarize
SrcIpAddrsummarize
IPCustomEntityextend

API - Password Cracking

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

'42Crunch API protection against password cracking'

MITRE ATT&CK Coverage

Rule Body

id: d951d64d-0ecd-4675-8c79-6c870d5f72ac
name: API - Password Cracking
description: |
  '42Crunch API protection against password cracking'
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
relevantTechniques:
  - T1110
  - T1555
  - T1187
query: |
  let loginRec = 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 == 403;
  let recCount = iff((toscalar(loginRec | count) > 10), 1, 0);
  loginRec | 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 loginRec is inlined into the numbered stages below.

Let Binding: recCount used in Stage 5

let recCount = iff((toscalar(loginRec | count) > 10), 1, 0);

Stages 1 to 4 define let loginRec as the rule's main pipeline source. Stage 5 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 == 403

Stage 5: take

loginRec
| take recCount

Indicators

These rows show field, operator, and value matches.

BloodHound Attack Path Finding - Admins on Tier Zero Computers

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

BloodHound Attack Path Finding - Admins on Tier Zero Computers

Rule Body

id: b0b747f2-938a-4f13-b7d1-9def60a719bd
name: BloodHound Attack Path Finding - Admins on Tier Zero Computers
description: BloodHound Attack Path Finding - Admins on Tier Zero Computers
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"T0Admins\""
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0Admins"

Indicators

These rows show field, operator, and value matches.

Cloudflare - WAF Allowed threat

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

'Detects WAF "Allowed" action on threat events.'

MITRE ATT&CK Coverage

Rule Body

id: f53fe2a9-96b5-454c-827e-cf1764a67fb1
name: Cloudflare - WAF Allowed threat
description: |
  'Detects WAF "Allowed" action on threat events.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  Cloudflare
  | where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)
  | where WAFAction =~ 'Allow'
  | extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: CompleteUrl
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

Cloudflare

Stage 2: where

| where isnotempty(WAFRuleID) or isnotempty(WAFRuleMessage)

Stage 3: where

| where WAFAction =~ 'Allow'

Stage 4: extend

| extend CompleteUrl = strcat(HttpRequestHeaderHost,ClientRequestPath)

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CompleteUrlextend

Common Event Format (CEF) via AMA - Critical or High Severity Detections by User

#
Status
available
Severity
high
Time window
1h
Group by
DstHostName, DstUserName, FileHash, FileName, Message, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

'Creates an incident when a large number of Critical or High severity CrowdStrike Falcon sensor detections is triggered by a single user within 1 hour. The rule uses the CrowdStrikeFalconEventStream table, filters for DetectionSummaryEvent records with Severity set to Critical or High, and alerts when detections for a single DstUserName exceed the configured threshold of 15. Review DstHostName, SrcIpAddr, FileName, FileHash, and Message for investigation context.'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment
Impact

Rule Body

id: 4465ebde-b381-45f7-ad08-7d818070a11c
name: Common Event Format (CEF) via AMA - Critical or High Severity Detections by User
description: |
  'Creates an incident when a large number of Critical or High severity CrowdStrike Falcon sensor detections is triggered by a single user within 1 hour. The rule uses the CrowdStrikeFalconEventStream table, filters for DetectionSummaryEvent records with Severity set to Critical or High, and alerts when detections for a single DstUserName exceed the configured threshold of 15. Review DstHostName, SrcIpAddr, FileName, FileHash, and Message for investigation context.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: 
- Impact
- DefenseEvasion
relevantTechniques:
- T1489
- T1562
query: |
  let timeframe = 1h;
  let threshold = 15; // update threshold value based on organization's preference
  let NotableEvents = CrowdStrikeFalconEventStream
  | where TimeGenerated > ago(timeframe)
  | where EventType == "DetectionSummaryEvent"
  | where Severity in ("Critical", "High")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Total = count() by DstUserName, DstHostName, SrcIpAddr, FileName, FileHash, Message
  | where Total > threshold;
  NotableEvents
  | extend timestamp = StartTimeUtc, AccountCustomEntity = DstUserName, HostCustomEntity = DstHostName, IPCustomEntity = SrcIpAddr, FileHashCustomEntity = FileHash, FileHashAlgo = "MD5"
  | project timestamp, StartTimeUtc, EndTimeUtc, DstUserName, DstHostName, SrcIpAddr, FileName, FileHash, FileHashAlgo, Message, Total, AccountCustomEntity, HostCustomEntity, IPCustomEntity, FileHashCustomEntity
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: AccountCustomEntity
- entityType: Host
  fieldMappings:
  - identifier: FullName
    columnName: HostCustomEntity
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IPCustomEntity
- entityType: FileHash
  fieldMappings:
  - identifier: Algorithm
    columnName: FileHashAlgo
  - identifier: Value
    columnName: FileHashCustomEntity
alertDetailsOverride:
  alertDisplayNameFormat: 'CrowdStrike critical/high detections by user: {{DstUserName}}'
  alertDescriptionFormat: User {{DstUserName}} generated {{Total}} critical/high detections from host {{DstHostName}}.
customDetails:
  DstUserName: DstUserName
  DstHostName: DstHostName
  SrcIpAddr: SrcIpAddr
  FileName: FileName
  FileHash: FileHash
  Total: Total
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1h;
let threshold = 15;

let NotableEvents is inlined into the numbered stages below.

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

Stage 1: source

CrowdStrikeFalconEventStream

Stage 2: where

| where TimeGenerated > ago(timeframe)

Stage 3: where

| where EventType == "DetectionSummaryEvent"

Stage 4: where

| where Severity in ("Critical", "High")

Stage 5: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Total = count() by DstUserName, DstHostName, SrcIpAddr, FileName, FileHash, Message

Stage 6: where

| where Total > threshold

Stage 7: extend

NotableEvents
| extend timestamp = StartTimeUtc, AccountCustomEntity = DstUserName, HostCustomEntity = DstHostName, IPCustomEntity = SrcIpAddr, FileHashCustomEntity = FileHash, FileHashAlgo = "MD5"

Stage 8: project

| project timestamp, StartTimeUtc, EndTimeUtc, DstUserName, DstHostName, SrcIpAddr, FileName, FileHash, FileHashAlgo, Message, Total, AccountCustomEntity, HostCustomEntity, IPCustomEntity, FileHashCustomEntity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityproject
DstHostNameproject
DstUserNameproject
EndTimeUtcproject
FileHashproject
FileHashAlgoproject
FileHashCustomEntityproject
FileNameproject
HostCustomEntityproject
IPCustomEntityproject
Messageproject
SrcIpAddrproject
StartTimeUtcproject
Totalproject
timestampproject

CyberArkEPM - Process started from different locations

#
Severity
medium
Time window
1h
Group by
ActingProcessFileInternalName, DvcId
Source
github.com/Azure/Azure-Sentinel

'Detects when process started from different locations on a host.'

MITRE ATT&CK Coverage

TacticTechniques
Execution
Stealth

Rule Body

id: 0d4e62da-0a64-4532-b93e-28cd2940c300
name: CyberArkEPM - Process started from different locations
description: |
  'Detects when process started from different locations on a host.'
severity: Medium
requiredDataConnectors:
  - connectorId: CyberArkEPM
    dataTypes:
      - CyberArkEPM
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - DefenseEvasion
relevantTechniques:
  - T1204
  - T1036
query: | 
  CyberArkEPM
  | where EventSubType != 'AttackAttempt'
  | extend bin_path = tolower(extract(@'\A(.*)(\\|/)', 1, ActingProcessName))
  | summarize p = makeset(bin_path) by ActingProcessFileInternalName, DvcId
  | where array_length(p) > 1
  | extend FileCustomEntity = ActingProcessFileInternalName
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CyberArkEPM

Stage 2: where

| where EventSubType != 'AttackAttempt'

Stage 3: extend

| extend bin_path = tolower(extract(@'\A(.*)(\\|/)', 1, ActingProcessName))

Stage 4: summarize

| summarize p = makeset(bin_path) by ActingProcessFileInternalName, DvcId

Stage 5: where

| where array_length(p) > 1

Stage 6: extend

| extend FileCustomEntity = ActingProcessFileInternalName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActingProcessFileInternalNamesummarize
DvcIdsummarize
psummarize
FileCustomEntityextend

Detect executable drops via Azure custom script extension

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

This detection rule flags when the Custom Script extension service on a machine is dropping executable files. This might indicate that an actor is trying to drop malware or beacons via a compromised cloud admin account. In the most legitimate cases administrators are pushing only PowerShell or Shell scripts, although these can also contain malicious content. Be aware of this gap in the below detection rule.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

// Executable extensions we want to flag (you can also add .ps1 and .sh)
let win_executable_extensions = dynamic([".dll", ".exe", ".msi", ".bat", ".cmd", ".com", ".vbs", ".wsf", ".scr", ".cpl"]);
DeviceFileEvents
| where TimeGenerated > ago(1h)
// Search for file created events by Arc Custom Script Handler
| where ActionType == "FileCreated"
| where InitiatingProcessFileName =~ "customscripthandler.exe"
// Get the file type
| extend FileType = tostring(parse_json(AdditionalFields).FileType)
// Flag on extension or executable file type
| where FileName has_any (win_executable_extensions) or 
    FileType contains "Executable"

Stages and Predicates

Parameters

let win_executable_extensions = dynamic([".dll", ".exe", ".msi", ".bat", ".cmd", ".com", ".vbs", ".wsf", ".scr", ".cpl"]);

Stage 1: source

DeviceFileEvents

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where ActionType == "FileCreated"

Stage 4: where

| where InitiatingProcessFileName =~ "customscripthandler.exe"

Stage 5: extend

| extend FileType = tostring(parse_json(AdditionalFields).FileType)

Stage 6: where

| where FileName has_any (win_executable_extensions) or 
    FileType contains "Executable"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • FileCreated corpus 2 (kusto 2)
field:"ActionType" kind:eq value:"FileCreated"
FileNamematch
  • .bat transforms: term
  • .cmd transforms: term
  • .com transforms: term
  • .cpl transforms: term
  • .dll transforms: term
  • .exe transforms: term
  • .msi transforms: term
  • .scr transforms: term
  • .vbs transforms: term
  • .wsf transforms: term
field:"file_name" kind:match
FileTypecontains
  • Executable
field:"FileType" kind:contains value:"Executable"
InitiatingProcessFileNameeq
  • customscripthandler.exe corpus 2 (elastic 1, kusto 1)
field:"process_basename" kind:eq value:"customscripthandler.exe"

Output Fields

These fields are emitted when the rule matches.

FieldSource
FileTypeextend

Netskope - Impossible Travel Detection (Two Countries in Less Than 1 Hour)

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

Detects when a user accesses resources from two distinct countries within less than 1 hour, indicating potential credential compromise or VPN abuse.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: d1b88716-3cd4-4585-a9a2-2dd2c9b04ecb
name: Netskope - Impossible Travel Detection (Two Countries in Less Than 1 Hour)
description: |
  Detects when a user accesses resources from two distinct countries within less than 1 hour, indicating potential credential compromise or VPN abuse.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: NetskopeWebTxConnector
    dataTypes:
      - NetskopeWebTransactions_CL
queryFrequency: 1h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
query: |
  let timeWindow = 1h;
  NetskopeWebTransactions_CL
  | where TimeGenerated > ago(1h)
  | where isnotempty(CsUsername) and isnotempty(XCCountry)
  | summarize 
      Countries = make_set(XCCountry),
      Locations = make_set(XCLocation),
      IPs = make_set(CIp),
      MinTime = min(TimeGenerated),
      MaxTime = max(TimeGenerated),
      EventCount = count()
      by CsUsername, bin(TimeGenerated, timeWindow)
  | where array_length(Countries) > 1
  | extend TimeDiffMinutes = datetime_diff('minute', MaxTime, MinTime)
  | where TimeDiffMinutes < 60
  | project 
      TimeGenerated,
      User = CsUsername,
      DistinctCountries = Countries,
      Locations,
      SourceIPs = IPs,
      TimeDifferenceMinutes = TimeDiffMinutes,
      EventCount,
      AlertDetails = strcat('User ', CsUsername, ' accessed from ', array_length(Countries), ' countries within ', TimeDiffMinutes, ' minutes')
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: User
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let timeWindow = 1h;

Stage 1: source

NetskopeWebTransactions_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where isnotempty(CsUsername) and isnotempty(XCCountry)

Stage 4: summarize

| summarize 
    Countries = make_set(XCCountry),
    Locations = make_set(XCLocation),
    IPs = make_set(CIp),
    MinTime = min(TimeGenerated),
    MaxTime = max(TimeGenerated),
    EventCount = count()
    by CsUsername, bin(TimeGenerated, timeWindow)

Stage 5: where

| where array_length(Countries) > 1

Stage 6: extend

| extend TimeDiffMinutes = datetime_diff('minute', MaxTime, MinTime)

Stage 7: where

| where TimeDiffMinutes < 60

Stage 8: project

| project 
    TimeGenerated,
    User = CsUsername,
    DistinctCountries = Countries,
    Locations,
    SourceIPs = IPs,
    TimeDifferenceMinutes = TimeDiffMinutes,
    EventCount,
    AlertDetails = strcat('User ', CsUsername, ' accessed from ', array_length(Countries), ' countries within ', TimeDiffMinutes, ' minutes')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CsUsernameis_not_null
  • (no value, null check)
field:"CsUsername" kind:is_not_null
TimeDiffMinuteslt
  • 60 transforms: number
field:"TimeDiffMinutes" kind:lt value:"60"
XCCountryis_not_null
  • (no value, null check)
field:"XCCountry" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertDetailsproject
DistinctCountriesproject
EventCountproject
Locationsproject
SourceIPsproject
TimeDifferenceMinutesproject
TimeGeneratedproject
Userproject

NordPass - Domain data detected in breach

#
Severity
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

This will alert you when Data Breach Scanner discovers data related to your organization's domains on the dark web. !This rule should be enabled only by the organizations that have set up Data Breach Scanner in NordPass.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: e3f2b6c9-df0c-4b36-a376-bb2762e4dbdc
name: NordPass - Domain data detected in breach
kind: Scheduled
version: 1.0.0
displayName: Domain data detected in breach
description: |
  This will alert you when Data Breach Scanner discovers data related to your organization's domains on the dark web.
  
  !This rule should be enabled only by the organizations that have set up Data Breach Scanner in NordPass.
severity: High
query: |
  NordPassEventLogs_CL
  | where event_type == "breach"
  | where action == "domain_breached"
  | extend TargetEmail = user_email
requiredDataConnectors:
  - connectorId: NordPass
    dataTypes:
      - NordPassEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1020
incidentConfiguration:
  createIncident: false
entityMappings:
- entityType: Mailbox
  fieldMappings:
  - identifier: MailboxPrimaryAddress
    columnName: TargetEmail

Stages and Predicates

Stage 1: source

NordPassEventLogs_CL

Stage 2: where

| where event_type == "breach"

Stage 3: where

| where action == "domain_breached"

Stage 4: extend

| extend TargetEmail = user_email

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
TargetEmailextend

NRT Process executed from binary hidden in Base64 encoded file

#
Status
available
Severity
medium
Source
github.com/Azure/Azure-Sentinel

Encoding malicious software is a technique used to obfuscate files from detection. The first CommandLine component is looking for Python decoding base64. The second CommandLine component is looking for Bash/sh command line base64 decoding. The third one is looking for Ruby decoding base64.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 7ad4c32b-d0d2-411c-a0e8-b557afa12fce
name: NRT Process executed from binary hidden in Base64 encoded file
description: |
  'Encoding malicious software is a technique used to obfuscate files from detection.
  The first CommandLine component is looking for Python decoding base64.
  The second CommandLine component is looking for Bash/sh command line base64 decoding.
  The third one is looking for Ruby decoding base64.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
tactics:
  - Execution
  - DefenseEvasion
relevantTechniques:
  - T1059
  - T1027
  - T1140
query: |
  SecurityEvent
  | where EventID==4688
  | where isnotempty(CommandLine)
  | project TimeGenerated, Computer, AccountName = SubjectUserName, AccountNTDomain = SubjectDomainName, FileName = Process, CommandLine, ParentProcessName, SubjectAccount
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | where CommandLine contains ".decode('base64')"
          or CommandLine contains "base64 --decode"
          or CommandLine contains ".decode64("
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SubjectAccount
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.2
kind: NRT

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID==4688

Stage 3: where

| where isnotempty(CommandLine)

Stage 4: project

| project TimeGenerated, Computer, AccountName = SubjectUserName, AccountNTDomain = SubjectDomainName, FileName = Process, CommandLine, ParentProcessName, SubjectAccount

Stage 5: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 6: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
HostNameDomain =
ifDomainIndex != -1substring(Computer, (DomainIndex + 1))
elseComputer

Stage 7: where

| where CommandLine contains ".decode('base64')"
        or CommandLine contains "base64 --decode"
        or CommandLine contains ".decode64("

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinecontains
  • .decode('base64')
  • .decode64(
  • base64 --decode corpus 3 (elastic 2, kusto 1)
field:"CommandLine" kind:contains
CommandLineis_not_null
  • (no value, null check)
field:"CommandLine" kind:is_not_null
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNTDomainproject
AccountNameproject
CommandLineproject
Computerproject
FileNameproject
ParentProcessNameproject
SubjectAccountproject
TimeGeneratedproject
DomainIndexextend
HostNameextend
HostNameDomainextend

Potential Kerberos Relaying Activity - MDE

#
Time window
20s
Group by
DeviceId, DeviceName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

The below query detects potential Kerberos relaying event chain generated by KrbRelay.

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique

References

Telemetry Coverage

Rule Body

// Author       : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/detecting-kerberos-relaying-e6be66fa647c
//
// Description: This query detects potential Kerberos relaying event chain generated by KrbRelay(https://github.com/cube0x0/KrbRelay).
//
// Query parameters:
//
union DeviceProcessEvents, DeviceEvents, DeviceNetworkEvents
| where Timestamp > ago(60m)
| extend PipeName_ = tostring(todynamic(AdditionalFields).PipeName)
| extend Action = case(
    (ActionType=="ConnectionSuccess" and RemotePort in (389, 636)), 'ConnectToDC', 
    (ActionType=="ListeningConnectionCreated"), 'ListeningConnectionCreated', 
    (ActionType=="NamedPipeEvent" and PipeName_ has @"Winsock2\CatalogChangeListener"), 'NamedPipeEvent',
    (ActionType=="ProcessCreated" and InitiatingProcessCommandLine == "svchost.exe -k DcomLaunch -p"), 'DcomProcessCreated',
    (ActionType=="ConnectionSuccess" and LocalIP==RemoteIP and InitiatingProcessParentFileName=="svchost.exe"), 'DcomProcessConnecToSelf', 'Other'
    )
| summarize Actions = make_set_if(Action, Action <> "Other") by DeviceId, DeviceName, bin(Timestamp,20s)
| where array_length(Actions)==5
| project-rename ActivityTimestamp = Timestamp
| join kind=inner (
    DeviceProcessEvents
    | where Timestamp > ago(60m)
    | where ActionType=="ProcessCreated" and InitiatingProcessCommandLine == "svchost.exe -k DcomLaunch -p"
    ) on DeviceId
    | where abs(datetime_diff('second', Timestamp, ActivityTimestamp)) <=20

union DeviceProcessEvents, DeviceEvents, DeviceNetworkEvents
| where Timestamp > ago(60m)
| extend PipeName_ = tostring(todynamic(AdditionalFields).PipeName)
| where 
    (ActionType=="ConnectionSuccess" and RemotePort in (389, 636)) or 
    (ActionType=="ListeningConnectionCreated") or 
    (ActionType=="NamedPipeEvent" and PipeName_ has @"Winsock2\CatalogChangeListener") or
    (ActionType=="ProcessCreated" and InitiatingProcessCommandLine == "svchost.exe -k DcomLaunch -p") or
    (ActionType=="ConnectionSuccess" and LocalIP==RemoteIP and InitiatingProcessParentFileName=="svchost.exe")
    | sort by Timestamp desc

Stages and Predicates

Union (3 Sources)

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

Leg 1: DeviceProcessEvents

Leg 2: DeviceEvents

Leg 3: DeviceNetworkEvents

Applied to the Combined Result

| where Timestamp > ago(60m) | extend PipeName_ = tostring(todynamic(AdditionalFields).PipeName) | extend Action = case(
    (ActionType=="ConnectionSuccess" and RemotePort in (389, 636)), 'ConnectToDC', 
    (ActionType=="ListeningConnectionCreated"), 'ListeningConnectionCreated', 
    (ActionType=="NamedPipeEvent" and PipeName_ has @"Winsock2\CatalogChangeListener"), 'NamedPipeEvent',
    (ActionType=="ProcessCreated" and InitiatingProcessCommandLine == "svchost.exe -k DcomLaunch -p"), 'DcomProcessCreated',
    (ActionType=="ConnectionSuccess" and LocalIP==RemoteIP and InitiatingProcessParentFileName=="svchost.exe"), 'DcomProcessConnecToSelf', 'Other'
    ) | summarize Actions = make_set_if(Action, Action <> "Other") by DeviceId, DeviceName, bin(Timestamp,20s) | where array_length(Actions)==5 | project-rename ActivityTimestamp = Timestamp | join kind=inner (
    DeviceProcessEvents
    | where Timestamp > ago(60m)
    | where ActionType=="ProcessCreated" and InitiatingProcessCommandLine == "svchost.exe -k DcomLaunch -p"
    ) on DeviceId | where abs(datetime_diff('second', Timestamp, ActivityTimestamp)) <=20

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionssummarize
DeviceIdsummarize
DeviceNamesummarize
ActivityTimestampproject-rename

Server Network Connection Anomalies

#
Group by
RemoteIP
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Servers have a specific baseline. This makes it easy to create a baseline and detect anomalies.
Below queries analyze the network connections made by the specified servers and detects the rare/anomalous ones.
You can add process info to the analysis, but it will probably generate more results(different processes for the same IP).

Telemetry Coverage

Rule Body

// Define servers you want to monitor. 
let Servers = dynamic(["server1","server2","etc."]);
// Get rare connections by RemoteIP and InitiatingProcessFileName
DeviceNetworkEvents
| where Timestamp > ago(30d)
| where DeviceName in (Servers) and ActionType == "ConnectionSuccess"
| where RemoteIPType !in ( "Private", "Loopback" )
| where RemoteIP !startswith "169.254."
| summarize make_set(RemoteUrl), count() by RemoteIP
| where count_ < 50
// Exclude traffic to known destinations.
| where not ( set_RemoteUrl has_any (".microsoft.com",".windowsupdate.com","login.microsoftonline.com","login.live.com","autodiscover-s.outlook.com","ocsp.digicert.com","ocsp.verisign.com","login.windows.net", "outlook.office365.com","accounts.accesscontrol.windows.net"))
// Get details of the connections that were made in the last 5 days.
// If you are going to check the results everyday, change the threshold to 1d. 
| join kind=inner
    (
    DeviceNetworkEvents
    | where Timestamp > ago(5d)
    | where DeviceName in (Servers) and ActionType == "ConnectionSuccess"
    | where RemoteIPType !in ( "Private", "Loopback" )
    | where RemoteIP !startswith "169.254."
    ) on RemoteIP

// Define servers you want to monitor. 
let Servers = dynamic(["server1","server2","etc."]);
// Get rare connections by DestinationIp
let _lookback = 30d;
let _timeframe = 5d;
let PrivateIPregex = @'^127\.|^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.';
// Parse Sysmon network connections and get only the ones towards the internet.
let parse_sysmon_id3 = (T:(TimeGenerated:datetime,EventID:int, Source:string,RenderedDescription:string, EventData:string))
{
T 
| where TimeGenerated > ago(_lookback)
| where Source == "Microsoft-Windows-Sysmon" and EventID == 3
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=tostring(['#text'])
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""),  TechniqueName = column_ifexists("TechniqueName", "")
| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
// Filter connections towards the internet
| where not (DestinationIp matches regex PrivateIPregex)
};
// Get rare connections by DestinationIp
Event
| where TimeGenerated > ago(_lookback)
| where Computer in (Servers)
| invoke parse_sysmon_id3()
| summarize count() by DestinationIp
| where count_ < 50
// get details of the rare connections for further analysis.
| join kind=inner
    (
    Event
    | where TimeGenerated > ago(_timeframe)
    | where Computer in (Servers)
    | invoke parse_sysmon_id3()
    ) on DestinationIp

Stages and Predicates

Parameters

let Servers = dynamic(["server1","server2","etc."]);

Stage 1: source

DeviceNetworkEvents

Stage 2: where

where Timestamp > ago(2592000s)

Stage 3: where

where ActionType == "ConnectionSuccess" and DeviceName in ("etc.", "server1", "server2")

Stage 4: where

where not (RemoteIPType in ("Loopback", "Private"))

Stage 5: where

where not (RemoteIP startswith "169.254.")

Stage 6: summarize

summarize by RemoteIP

Stage 7: where

where count_ < 50

Stage 8: where

where not ((set_RemoteUrl has ".microsoft.com" or set_RemoteUrl has ".windowsupdate.com" or set_RemoteUrl has "login.microsoftonline.com" or set_RemoteUrl has "login.live.com" or set_RemoteUrl has "autodiscover-s.outlook.com" or set_RemoteUrl has "ocsp.digicert.com" or set_RemoteUrl has "ocsp.verisign.com" or set_RemoteUrl has "login.windows.net" or set_RemoteUrl has "outlook.office365.com" or set_RemoteUrl has "accounts.accesscontrol.windows.net"))

Stage 9: join

join kind=inner (DeviceNetworkEvents) on RemoteIP

Stage 10: source

Event

Stage 11: where

where TimeGenerated > ago(2592000s)

Stage 12: where

where Computer in ("etc.", "server1", "server2")

Stage 13: where

where TimeGenerated > ago(2592000s)

Stage 14: where

where EventID == 3 and Source == "Microsoft-Windows-Sysmon"

Stage 15: extend

extend RenderedDescription

Stage 16: extend

extend EventData

Stage 17: mv-expand

mv-expand EventData

Stage 18: evaluate

evaluate

Stage 19: extend

extend Key, Value

Stage 20: evaluate

evaluate

Stage 21: extend

extend RuleName, TechniqueId, TechniqueName

Stage 22: parse

parse

Stage 23: where

where not (DestinationIp matches regex @"^127\.|^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.")

Stage 24: summarize

summarize by DestinationIp

Stage 25: where

where count_ < 50

Stage 26: join

join kind=inner (Event) on DestinationIp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
RemoteIPTypeinLoopback, Privateexcludes:RemoteIPType field:"RemoteIPType" value:"Loopback" field:"RemoteIPType" value:"Private"
RemoteIPstarts_with169.254.excludes:RemoteIP field:"RemoteIP" value:"169.254."
set_RemoteUrlmatch.microsoft.com, .windowsupdate.com, login.microsoftonline.com, login.live.com, autodiscover-s.outlook.com, ocsp.digicert.com, ocsp.verisign.com, login.windows.net, outlook.office365.com, accounts.accesscontrol.windows.netexcludes:set_RemoteUrl
DestinationIpregex_match^127., ^10., ^172.1[6-9]., ^172.2[0-9]., ^172.3[0-1]., ^192.168.excludes:DestinationIp

Output Fields

These fields are emitted when the rule matches.

FieldSource
RemoteIPsummarize

Suspicious VM Instance Creation Activity Detected

#
Severity
medium
Time window
1d
Group by
$left.EntityIp, $right.GCPUserIp, AccountObjectId, AlertName, AlertSeverity, AlertTime, EntityIp, ProductName, ProviderName, Tactics, Techniques
Source
github.com/Azure/Azure-Sentinel

This detection identifies high-severity alerts across various Microsoft security products, including Microsoft Defender XDR and Microsoft Entra ID, and correlates them with instances of Google Cloud VM creation. It focuses on instances where VMs were created within a short timeframe of high-severity alerts, potentially indicating suspicious activity.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Execution
Discovery

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 1cc0ba27-c5ca-411a-a779-fbc89e26be83
name: Suspicious VM Instance Creation Activity Detected
description: |
  'This detection identifies high-severity alerts across various Microsoft security products, including Microsoft Defender XDR and Microsoft Entra ID, and correlates them with instances of Google Cloud VM creation. It focuses on instances where VMs were created within a short timeframe of high-severity alerts, potentially indicating suspicious activity.'
severity: Medium
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
  - connectorId: AzureActiveDirectoryIdentityProtection
    dataTypes:
      - SecurityAlert (IPC)
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
  - connectorId: MicrosoftDefenderAdvancedThreatProtection
    dataTypes:
      - SecurityAlert (MDATP)
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - Discovery
relevantTechniques:
  - T1078
  - T1106
  - T1526
query: |
    // Filter alerts from specific Microsoft security products with medium and high severity
    SecurityAlert 
    | where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security", "Azure Active Directory Identity Protection", "Microsoft Defender ATP")
    | where AlertSeverity has_any ("Medium", "High")
    // Parse JSON entities and extend AlertTimeGenerated
    | extend Entities = parse_json(Entities), AlertTimeGenerated=TimeGenerated
    // Extract and process IP entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'ip' 
        | extend EntityIp = tostring(Entity.Address) 
        ) 
    // Extract and process account entities
    | mv-apply Entity = Entities on 
        ( 
        where Entity.Type == 'account' 
        | extend AccountObjectId = tostring(Entity.AadUserId)
        )
    // Filter out records with empty EntityIp
    | where isnotempty(EntityIp)
    // Summarize data and create sets of entities and system alert IDs
    | summarize Entitys=make_set(Entity), SystemAlertIds=make_set(SystemAlertId)
        by 
        AlertName,
        ProductName,
        AlertSeverity,
        EntityIp,
        Tactics,
        Techniques,
        ProviderName,
        AlertTime= bin(AlertTimeGenerated, 1d),
        AccountObjectId
    // Join with GCPAuditLogs for VM instance creation
    | join kind=inner (
        GCPAuditLogs
        | where ServiceName == "compute.googleapis.com" and MethodName endswith "instances.insert"
        | extend
            GCPUserUPN= tostring(parse_json(AuthenticationInfo).principalEmail),
            GCPUserIp = tostring(parse_json(RequestMetadata).callerIp),
            GCPUserUA= tostring(parse_json(RequestMetadata).callerSuppliedUserAgent),
            VMStatus =  tostring(parse_json(Response).status),
            VMOperation=tostring(parse_json(Response).operationType),
            VMName= tostring(parse_json(Request).name),
            VMType = tostring(split(parse_json(Request).machineType, "/")[-1])
        | where GCPUserUPN !has "gserviceaccount.com"
        | where VMOperation == "insert" and isnotempty(GCPUserIp) and GCPUserIp != "private"
        | project
            GCPOperationTime=TimeGenerated,
            VMName,
            VMStatus,
            MethodName,
            GCPUserUPN,
            ProjectId,
            GCPUserIp,
            GCPUserUA,
            VMOperation,
            VMType
        )
        on $left.EntityIp == $right.GCPUserIp 
    // Join with IdentityInfo to enrich user identity details
    | join kind=inner (IdentityInfo 
        | distinct AccountObjectId, AccountUPN, JobTitle
        )
        on AccountObjectId 
    // Calculate the time difference between the alert and VM creation for further analysis
    | extend TimeDiff= datetime_diff('day', AlertTime, GCPOperationTime),Name = split(GCPUserUPN, "@")[0], UPNSuffix = split(GCPUserUPN, "@")[1]
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: GCPUserIp
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: GCPUserUPN
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
customDetails:
  AlertName: AlertName
  AlertProDuctName: ProductName
  AlertUserName: AccountUPN
  AlertUserObjectId: AccountObjectId
  AlertIds: SystemAlertIds
  AlertIp: EntityIp
  GCPUserAgent: GCPUserUA
  GCPVMName: VMName
  GCPProjectId: ProjectId
  GCPVMType: VMType
  CorrelationWith: "GCPAuditLogs"
alertDetailsOverride:
  alertDisplayNameFormat: "IP address {{GCPUserIp}} Assocated with {{AlertName}} found in GCP VM creation event by {{GCPUserUPN}}"
  alertDescriptionFormat: "This detection correlates '{{ProductName}}' Alert IP addresse Entity found in VM instance creation in GCP {{ProjectId}}. It identifies successful compute instance creation, from suspicious IP addresse. By joining these datasets on network entities and IP addresses, it detects unauthorized Initial access attempts across GCP environments."
  alertSeverityColumnName: AlertSeverity
  alertDynamicProperties:
    - alertProperty: ProviderName
      value: "Microsoft Security"
    - alertProperty: ProductName
      value: "Microsoft Defender"
    - alertProperty: ProductComponentName
      value: "Microsoft Defender"
kind: Scheduled
version: 1.0.4

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProductName in ("Microsoft 365 Defender", "Azure Active Directory", "Microsoft Defender Advanced Threat Protection", "Microsoft Cloud App Security", "Azure Active Directory Identity Protection", "Microsoft Defender ATP")

Stage 3: where

| where AlertSeverity has_any ("Medium", "High")

Stage 4: extend

| extend Entities = parse_json(Entities), AlertTimeGenerated=TimeGenerated

Stage 5: kusto:mv-apply

| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'ip' 
    | extend EntityIp = tostring(Entity.Address) 
    )

Stage 6: kusto:mv-apply

| mv-apply Entity = Entities on 
    ( 
    where Entity.Type == 'account' 
    | extend AccountObjectId = tostring(Entity.AadUserId)
    )

Stage 7: where

| where isnotempty(EntityIp)

Stage 8: summarize

| summarize Entitys=make_set(Entity), SystemAlertIds=make_set(SystemAlertId)
    by 
    AlertName,
    ProductName,
    AlertSeverity,
    EntityIp,
    Tactics,
    Techniques,
    ProviderName,
    AlertTime= bin(AlertTimeGenerated, 1d),
    AccountObjectId

Stage 9: join

| join kind=inner (
    GCPAuditLogs
    | where ServiceName == "compute.googleapis.com" and MethodName endswith "instances.insert"
    | extend
        GCPUserUPN= tostring(parse_json(AuthenticationInfo).principalEmail),
        GCPUserIp = tostring(parse_json(RequestMetadata).callerIp),
        GCPUserUA= tostring(parse_json(RequestMetadata).callerSuppliedUserAgent),
        VMStatus =  tostring(parse_json(Response).status),
        VMOperation=tostring(parse_json(Response).operationType),
        VMName= tostring(parse_json(Request).name),
        VMType = tostring(split(parse_json(Request).machineType, "/")[-1])
    | where GCPUserUPN !has "gserviceaccount.com"
    | where VMOperation == "insert" and isnotempty(GCPUserIp) and GCPUserIp != "private"
    | project
        GCPOperationTime=TimeGenerated,
        VMName,
        VMStatus,
        MethodName,
        GCPUserUPN,
        ProjectId,
        GCPUserIp,
        GCPUserUA,
        VMOperation,
        VMType
    )
    on $left.EntityIp == $right.GCPUserIp

Stage 10: join

| join kind=inner (IdentityInfo 
    | distinct AccountObjectId, AccountUPN, JobTitle
    )
    on AccountObjectId

Stage 11: extend

extend Name, TimeDiff, UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertSeveritymatch
  • High transforms: term
  • Medium transforms: term
field:"AlertSeverity" kind:match
Entity.Typeeq
  • account transforms: kusto_dynamic_path
  • ip transforms: kusto_dynamic_path
field:"Entity.Type" kind:eq
EntityIpis_not_null
  • (no value, null check)
field:"EntityIp" kind:is_not_null
GCPUserIpis_not_null
  • (no value, null check)
field:"GCPUserIp" kind:is_not_null
GCPUserIpne
  • private
field:"GCPUserIp" kind:ne value:"private"
MethodNameends_with
  • instances.insert
field:"MethodName" kind:ends_with value:"instances.insert"
ProductNamein
  • Azure Active Directory corpus 2 (kusto 2)
  • Azure Active Directory Identity Protection corpus 3 (kusto 3)
  • Microsoft 365 Defender corpus 2 (kusto 2)
  • Microsoft Cloud App Security corpus 3 (kusto 3)
  • Microsoft Defender ATP corpus 2 (kusto 2)
  • Microsoft Defender Advanced Threat Protection corpus 2 (kusto 2)
field:"ProductName" kind:in
ServiceNameeq
  • compute.googleapis.com
field:"ServiceName" kind:eq value:"compute.googleapis.com"
VMOperationeq
  • insert
field:"VMOperation" kind:eq value:"insert"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
GCPUserUPNmatchgserviceaccount.comexcludes:GCPUserUPN field:"GCPUserUPN" value:"gserviceaccount.com"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountObjectIdsummarize
AlertNamesummarize
AlertSeveritysummarize
AlertTimesummarize
EntityIpsummarize
Entityssummarize
ProductNamesummarize
ProviderNamesummarize
SystemAlertIdssummarize
Tacticssummarize
Techniquessummarize
Nameextend
TimeDiffextend
UPNSuffixextend

Tailscale: DNS nameservers modified

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

Identifies when the tailnet's global DNS nameserver list is modified. Adding an attacker-controlled resolver as a tailnet-wide nameserver enables broad DNS hijacking for every device using MagicDNS resolution.

MITRE ATT&CK Coverage

Rule Body

id: c5d6e7f8-2345-6789-01ab-cdef12345011
name: "Tailscale: DNS nameservers modified"
description: |
  Identifies when the tailnet's global DNS nameserver list is modified. Adding an attacker-controlled resolver as a tailnet-wide nameserver enables broad DNS hijacking for every device using MagicDNS resolution.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - CommandAndControl
relevantTechniques:
  - T1556
  - T1568
query: |
  Tailscale_Audit_CL
  | where Action == "UPDATE"
  | where tostring(Target.type) == "TAILNET"
  | where tostring(Target.property) == "DNS_NAMESERVERS"
  | extend ActorLogin = tostring(Actor.loginName)
  | extend OldServers = Old.dns
  | extend NewServers = New.dns
  | extend Added = set_difference(NewServers, OldServers)
  | extend Removed = set_difference(OldServers, NewServers)
  | project TimeGenerated, ActorLogin, OldServers, NewServers, Added, Removed, Origin
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action == "UPDATE"

Stage 3: where

| where tostring(Target.type) == "TAILNET"

Stage 4: where

| where tostring(Target.property) == "DNS_NAMESERVERS"

Stage 5: extend (5 consecutive steps)

| extend ActorLogin = tostring(Actor.loginName)
| extend OldServers = Old.dns
| extend NewServers = New.dns
| extend Added = set_difference(NewServers, OldServers)
| extend Removed = set_difference(OldServers, NewServers)

Stage 6: project

| project TimeGenerated, ActorLogin, OldServers, NewServers, Added, Removed, Origin

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • UPDATE
field:"Action" kind:eq value:"UPDATE"
Target.propertyeq
  • DNS_NAMESERVERS transforms: tostring, kusto_dynamic_path
field:"Target.property" kind:eq value:"DNS_NAMESERVERS"
Target.typeeq
  • TAILNET transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:eq value:"TAILNET"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorLoginproject
Addedproject
NewServersproject
OldServersproject
Originproject
Removedproject
TimeGeneratedproject

Tomcat - Server errors after multiple requests from same IP

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

'Detects server errors after multiple requests from same IP address.'

MITRE ATT&CK Coverage

Rule Body

id: 875da588-4875-11ec-81d3-0242ac130003
name: Tomcat - Server errors after multiple requests from same IP
description: |
  'Detects server errors after multiple requests from same IP address.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - Tomcat_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - InitialAccess
relevantTechniques:
  - T1498
  - T1190
  - T1133
query: |
  let multi_errors = TomcatEvent
  | where toint(HttpStatusCode) >= 500 and toint(HttpStatusCode) <= 599
  | sort by EventStartTime asc
  | summarize MultipleServerErrors = count() by SrcIpAddr, bin(EventStartTime, 5m)
  | where MultipleServerErrors > 10;
  let error_time_table = TomcatEvent
  | where toint(HttpStatusCode) >= 500 and toint(HttpStatusCode) <= 599
  | summarize error_time=min(EventStartTime) by SrcIpAddr
  | join kind=innerunique (multi_errors) on SrcIpAddr;
  TomcatEvent
  | where toint(HttpStatusCode) >= 100 and toint(HttpStatusCode) <= 399
  | summarize success_time=max(EventStartTime) by SrcIpAddr
  | join kind=innerunique (error_time_table) on SrcIpAddr
  | extend time_between_error_and_success = datetime_diff('second', error_time, success_time)
  | where time_between_error_and_success between (1 .. 300)
  | project time_between_error_and_success, error_time, success_time, SrcIpAddr
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: multi_errors

let multi_errors = TomcatEvent
| where toint(HttpStatusCode) >= 500 and toint(HttpStatusCode) <= 599
| sort by EventStartTime asc
| summarize MultipleServerErrors = count() by SrcIpAddr, bin(EventStartTime, 5m)
| where MultipleServerErrors > 10;

Let Binding: error_time_table used in Stage 4

let error_time_table = TomcatEvent
| where toint(HttpStatusCode) >= 500 and toint(HttpStatusCode) <= 599
| summarize error_time=min(EventStartTime) by SrcIpAddr
| join kind=innerunique (multi_errors) on SrcIpAddr;

Stage 1: source

TomcatEvent

Stage 2: where

| where toint(HttpStatusCode) >= 100 and toint(HttpStatusCode) <= 399

Stage 3: summarize

| summarize success_time=max(EventStartTime) by SrcIpAddr

Stage 4: join

join kind=innerunique (error_time_table) on SrcIpAddr

Stage 5: extend

extend time_between_error_and_success

Stage 6: where

where time_between_error_and_success >= 1 and time_between_error_and_success <= 300

Stage 7: project

project SrcIpAddr, error_time, success_time, time_between_error_and_success

Stage 8: extend

extend IPCustomEntity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrproject
error_timeproject
success_timeproject
time_between_error_and_successproject
IPCustomEntityextend

Vectra Create Incident Based on Tag for Hosts

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

Create an incident when the host entity presents a specific tag. If the tag is present, an incident should be created and marked with highest priority.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 8b7a1a64-8ef2-4000-b8c9-9bca3b93aace
name: Vectra Create Incident Based on Tag for Hosts
version: 1.1.0
kind: Scheduled
description: Create an incident when the host entity presents a specific tag. If the tag is present, an incident should be created and marked with highest priority.
status: Available
requiredDataConnectors:
  - connectorId: VectraXDR
    dataTypes:
      - Entities_Data_CL
severity: High
queryFrequency: 10m
queryPeriod: 10m
suppressionDuration: PT1H
suppressionEnabled: false
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Persistence
relevantTechniques:
  - T1546
query: |
  Entities_Data_CL
  | where entity_type == "host"
  | extend Tags = todynamic(tags)
  | where set_has_element(Tags, "MDR - Customer Escalation")
  | summarize arg_max(['last_modified_timestamp'], *) by ['name']
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: name
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AllEntities
    groupByEntities: [Host]
    groupByAlertDetails: [DisplayName]
customDetails:
  entity_id: id
  entity_type: entity_type
  tags: tags
  attack_profile: attack_profile
  ip_address: ip
alertDetailsOverride:
  alertDisplayNameFormat: Vectra AI Incident- {{name}} 
  alertDescriptionFormat: An incident has been escalated for Vectra AI entity {{name}} that is presenting an urgency score of {{urgency_score}}
  alertDynamicProperties:
    - alertProperty: "AlertLink"
      value: "url"
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Stage 1: source

Entities_Data_CL

Stage 2: where

| where entity_type == "host"

Stage 3: extend

| extend Tags = todynamic(tags)

Stage 4: where

| where set_has_element(Tags, "MDR - Customer Escalation")

Stage 5: summarize

| summarize arg_max(['last_modified_timestamp'], *) by ['name']

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Tagsin
  • MDR - Customer Escalation transforms: set_has_element
field:"Tags" kind:in value:"MDR - Customer Escalation"
entity_typeeq
  • host
field:"entity_type" kind:eq value:"host"

Back to all Kusto rules