Detection rules › Kusto

Kusto rules: 02

Archive Repository Settings Updated

#
Status
available
Severity
low
Time window
3h
Source
github.com/Azure/Azure-Sentinel

Detects when archive repository settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.

Rule Body

id: 74c3bda5-cdae-4af3-ab54-daddfbe8bc70
name: Archive Repository Settings Updated
description: Detects when archive repository settings are updated in Veeam Backup
  & Replication. This might indicate configuration changes that require review.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 29800\n| extend Name =  extract(\"\
  Name=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n    Date = format_datetime(TimeGenerated,\
  \ 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n    EventId = instanceId,\n\
  \    UserName = user,\n    [\"Object Name\"] = Name,\n   MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 29800

Stage 3: extend

| extend Name =  extract("Name=\"([^\"]*)\"", 1, SyslogMessage)

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = user,
    ["Object Name"] = Name,
   MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Object Nameproject
Severityproject
UserNameproject

Authentication Attempt from New Country

#
Severity
medium
Time window
14d
Group by
Category, Location, UserPrincipalName
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Detects when there is a login attempt from a country that has not seen a successful login in the previous 14 days. Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts. Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: ef895ada-e8e8-4cf0-9313-b1ab67fab69f
name: Authentication Attempt from New Country
description: |
  Detects when there is a login attempt from a country that has not seen a successful login in the previous 14 days.
  Threat actors may attempt to authenticate with credentials from compromised accounts - monitoring attempts from anomalous locations may help identify these attempts.
  Authentication attempts should be investigated to ensure the activity was legitimate and if there is other similar activity.
  Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#monitoring-for-failed-unusual-sign-ins
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let CombinedSignInLogs = union isfuzzy=True AADNonInteractiveUserSignInLogs, SigninLogs;
    // Combine AADNonInteractiveUserSignInLogs and SigninLogs into a single table
    // Fetch Azure IP address ranges data from a JSON file hosted on GitHub
    let AzureRanges = externaldata(changeNumber: string, cloud: string, values: dynamic)
    ["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
    // Load Azure IP address ranges from the JSON file hosted on GitHub
    | mv-expand values
    // Expand the values column into separate rows
    | extend Name = values.name, AddressPrefixes = tostring(values.properties.addressPrefixes);
    // Create additional columns for the name and address prefixes
    // Identify known locations to be excluded from analysis
    let ExcludedKnownLocations = CombinedSignInLogs
    // Filter the combined logs based on the specified time range
    | where TimeGenerated between (ago(14d)..ago(1d))
    // Filter by specific ResultType
    | where ResultType == 0
    // Summarize the logs by location
    | summarize by Location;
    // Find sign-in locations matching specific criteria
    let MatchedLocations = materialize(CombinedSignInLogs
    // Filter the combined logs based on the specified time range
    | where TimeGenerated > ago(1d)
    // Exclude specific ResultTypes
    | where ResultType !in (50126, 50053, 50074, 70044)
    // Exclude known locations
    | where Location !in (ExcludedKnownLocations));
    // Match IP addresses of matched locations with Azure IP address ranges
    let MatchedIPs = MatchedLocations
    // Use the 'ipv4_lookup' function to match IP addresses with Azure IP address ranges
    | evaluate ipv4_lookup(AzureRanges, IPAddress, AddressPrefixes)
    // Project only the IPAddress column
    | project IPAddress;
    // Exclude IP addresses that are already matched with Azure IP address ranges
    let MaxSetSize = 5; // Set the maximum size limit for make_set
    let ExcludedIPs = MatchedLocations
    // Filter out IP addresses that are already matched
    | where not (IPAddress in (MatchedIPs))
    // Exclude empty or null Location values
    | where isnotempty(Location)
    // Handle dynamic and string column values for LocationDetails and DeviceDetail
    | extend LocationDetails_dynamic = column_ifexists("LocationDetails_dynamic", "")
    | extend DeviceDetail_dynamic = column_ifexists("DeviceDetail_dynamic", "")
    | extend LocationDetails = iif(isnotempty(LocationDetails_dynamic), LocationDetails_dynamic, parse_json(LocationDetails_string))
    | extend DeviceDetail = iif(isnotempty(DeviceDetail_dynamic), DeviceDetail_dynamic, parse_json(DeviceDetail_string))
    // Extract location details (city and state)
    | extend City = tostring(LocationDetails.city)
    | extend State = tostring(LocationDetails.state)
    | extend Place = strcat(City, " - ", State)
    | extend DeviceId = tostring(DeviceDetail.deviceId)
    | extend Result = strcat(tostring(ResultType), " - ", ResultDescription)
    // Summarize the data based on UserPrincipalName, Location, and Category
    | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated),
    make_set(Result, MaxSetSize), make_set(IPAddress, MaxSetSize),
    make_set(UserAgent, MaxSetSize), make_set(Place, MaxSetSize),
    make_set(DeviceId, MaxSetSize) by UserPrincipalName, Location, Category
    // Extract the username prefix and suffix from UserPrincipalName
    | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0]);
    ExcludedIPs // Output the final result set
    | extend IP = set_IPAddress[0]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP
version: 1.1.2
kind: Scheduled
metadata:
  source:
    kind: Community
  author:
    name: Microsoft Security Research
  support:
    tier: Community
  categories:
    domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let MaxSetSize = 5;

let CombinedSignInLogs, let MatchedLocations and let ExcludedIPs are inlined into the numbered stages below.

Let Binding: AzureRanges

let AzureRanges = externaldata(changeNumber: string, cloud: string, values: dynamic)
  ["https://raw.githubusercontent.com/microsoft/mstic/master/PublicFeeds/MSFTIPRanges/ServiceTags_Public.json"] with(format='multijson')
  | mv-expand values
  | extend Name = values.name, AddressPrefixes = tostring(values.properties.addressPrefixes);

Let Binding: ExcludedKnownLocations used in Stage 6

let ExcludedKnownLocations = CombinedSignInLogs
  | where TimeGenerated between (ago(14d)..ago(1d))
  | where ResultType == 0
  | summarize by Location;

Let Binding: MatchedIPs used in Stage 7

let MatchedIPs = MatchedLocations
  | evaluate ipv4_lookup(AzureRanges, IPAddress, AddressPrefixes)
  | project IPAddress;

Stages 1 to 11 define let ExcludedIPs as the rule's main pipeline source. Stage 12 uses it.

Stage 1: union

union of 2 branches

Stage 2: source

AADNonInteractiveUserSignInLogs

Stage 3: source

SigninLogs

Stage 4: where

where TimeGenerated > ago(86400s)

Stage 5: where

where not (ResultType in (50053, 50074, 50126, 70044))

Stage 6: where

where not (Location == "ExcludedKnownLocations")

Stage 7: where

where not (IPAddress == "MatchedIPs")

Stage 8: where

where isnotempty(Location)

Stage 9: extend (9 consecutive steps)

extend City, DeviceDetail, DeviceDetail_dynamic, DeviceId, LocationDetails, LocationDetails_dynamic, Place, Result, State

Stage 10: summarize

summarize FirstSeen, LastSeen by UserPrincipalName, Location, Category

Stage 11: extend

extend Name, UPNSuffix

Stage 12: extend

extend IP

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Locationis_not_null
  • (no value, null check)
field:"Location" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ResultTypein50053, 50074, 50126, 70044excludes:ResultType
LocationeqExcludedKnownLocationsexcludes:Location field:"Location" value:"ExcludedKnownLocations"
IPAddresseqMatchedIPsexcludes:IPAddress field:"IPAddress" value:"MatchedIPs"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categorysummarize
FirstSeensummarize
LastSeensummarize
Locationsummarize
UserPrincipalNamesummarize
Nameextend
UPNSuffixextend
IPextend

Azure DevOps Pull Request Policy Bypassing - Historic allow list

#
Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'This detection builds an allow list of historic PR policy bypasses and compares to recent history, flagging pull request bypasses that are not manually in the allow list and not historically included in the allow list.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 4d8de9e6-263e-4845-8618-cd23a4f58b70
name: Azure DevOps Pull Request Policy Bypassing - Historic allow list
description: |
  'This detection builds an allow list of historic PR policy bypasses and compares to recent history, flagging pull request bypasses that are not manually in the allow list and not historically included in the allow list.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 3h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  let starttime = 14d;
  let endtime = 3h;
  // Add full UPN (user@domain.com) to Authorized Bypassers to ignore policy bypasses by certain authorized users
  let AuthorizedBypassers = dynamic(['foo@baz.com', 'test@foo.com']);
  let historicBypassers = ADOAuditLogs
  | where TimeGenerated between (ago(starttime) .. ago(endtime))
  | where OperationName == 'Git.RefUpdatePoliciesBypassed'
  | distinct ActorUPN;
  ADOAuditLogs
  | where TimeGenerated >= ago(endtime)
  | where OperationName == 'Git.RefUpdatePoliciesBypassed'
  | where ActorUPN !in (historicBypassers) and ActorUPN !in (AuthorizedBypassers)
  | parse ScopeDisplayName with OrganizationName '(Organization)'
  | project TimeGenerated, ActorUPN, IpAddress, UserAgent, OrganizationName, ProjectName, RepoName = Data.RepoName, AlertDetails = Details, Branch = Data.Name,
    BypassReason = Data.BypassReason, PRLink = strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_git/', Data.RepoName, '/pullrequest/', Data.PullRequestId)
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: PRLink
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 3h;
let AuthorizedBypassers = dynamic(['foo@baz.com', 'test@foo.com']);

Let Binding: historicBypassers used in Stage 4

let historicBypassers = ADOAuditLogs
| where TimeGenerated between (ago(starttime) .. ago(endtime))
| where OperationName == 'Git.RefUpdatePoliciesBypassed'
| distinct ActorUPN;

Stage 1: source

ADOAuditLogs

Stage 2: where

| where TimeGenerated >= ago(endtime)

Stage 3: where

| where OperationName == 'Git.RefUpdatePoliciesBypassed'

Stage 4: where

| where ActorUPN !in (historicBypassers) and ActorUPN !in (AuthorizedBypassers)

Stage 5: parse

| parse ScopeDisplayName with OrganizationName '(Organization)'

Stage 6: project

| project TimeGenerated, ActorUPN, IpAddress, UserAgent, OrganizationName, ProjectName, RepoName = Data.RepoName, AlertDetails = Details, Branch = Data.Name,
  BypassReason = Data.BypassReason, PRLink = strcat('https://dev.azure.com/', OrganizationName, '/', ProjectName, '/_git/', Data.RepoName, '/pullrequest/', Data.PullRequestId)

Stage 7: extend

| extend timestamp = TimeGenerated

Stage 8: extend

| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUPNproject
AlertDetailsproject
Branchproject
BypassReasonproject
IpAddressproject
OrganizationNameproject
PRLinkproject
ProjectNameproject
RepoNameproject
TimeGeneratedproject
UserAgentproject
timestampextend
AccountNameextend
AccountUPNSuffixextend

Cisco Cloud Security - Request to blocklisted file type

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

'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Command & Control

Rule Body

id: de58ee9e-b229-4252-8537-41a4c2f4045e
name: Cisco Cloud Security - Request to blocklisted file type
description: |
  'Detects request to potentially harmful file types (.ps1, .bat, .vbs, etc.).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes:
      - Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1189
  - T1105
query: |
  let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);
  Cisco_Umbrella
  | where EventType == 'proxylogs'
  | where DvcAction =~ 'Allowed'
  | extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)
  | extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)
  | where file_ext in (file_ext_blocklist)
  | project TimeGenerated, SrcIpAddr, DstIpAddr, Identities, Filename, UrlOriginal 
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlOriginal
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let file_ext_blocklist = dynamic(['.ps1', '.vbs', '.bat', '.scr']);

Stage 1: source

Cisco_Umbrella

Stage 2: where

| where EventType == 'proxylogs'

Stage 3: where

| where DvcAction =~ 'Allowed'

Stage 4: extend

| extend file_ext = extract(@'.*(\.\w+)$', 1, UrlOriginal)

Stage 5: extend

| extend Filename = extract(@'.*\/*\/(.*\.\w+)$', 1, UrlOriginal)

Stage 6: where

| where file_ext in (file_ext_blocklist)

Stage 7: project

| project TimeGenerated, SrcIpAddr, DstIpAddr, Identities, Filename, UrlOriginal

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstIpAddrproject
Filenameproject
Identitiesproject
SrcIpAddrproject
TimeGeneratedproject
UrlOriginalproject

CMMC 2.0 Level 1 (Foundational) Readiness Posture

#
Status
available
Severity
medium
Time window
7d
Group by
AssessedResourceId, Level, RecommendationName
Source
github.com/Azure/Azure-Sentinel

'CMMC 2.0 Level 1 (Foundational) assessments have deviated from configured threshold baselines. This alert is triggered when CMMC2.0 policy compliance is assessed below 70% compliance in 7 days.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: fb127436-e5c4-4e31-85a8-d3507128dd09
name: CMMC 2.0 Level 1 (Foundational) Readiness Posture
description: |
  'CMMC 2.0 Level 1 (Foundational) assessments have deviated from configured threshold baselines. This alert is triggered when CMMC2.0 policy compliance is assessed below 70% compliance in 7 days.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  SecurityRegulatoryCompliance
  | where ComplianceStandard == "NIST-SP-800-171-R2"
  | extend Level=iff(ComplianceControl in ("3.1.1","3.1.2","3.1.20","3.1.22","3.4.1","3.5.2","3.5.2","3.8.3","3.13.1","3.13.5","3.14.1","3.14.2","3.14.4","3.14.5"), "Level 1: Foundational","Level 2: Advanced")
  | where Level == "Level 1: Foundational"
  | summarize arg_max(TimeGenerated, *) by RecommendationName, AssessedResourceId, Level
  | summarize Failed=countif(State=="Failed"),Passed=countif(State=="Passed"),Total=countif(State=="Passed" or State == "Failed") by Level
  |extend PassedControlsPercentage = (Passed/todouble(Total))*100
  | where PassedControlsPercentage < 70 
  //Adjust Either Passed Thresholds within Organizational Needs
  | extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
  | project Level, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, LastObserved=now()
  | extend URLCustomEntity = RemediationLink
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityRegulatoryCompliance

Stage 2: where

| where ComplianceStandard == "NIST-SP-800-171-R2"

Stage 3: extend

| extend Level=iff(ComplianceControl in ("3.1.1","3.1.2","3.1.20","3.1.22","3.4.1","3.5.2","3.5.2","3.8.3","3.13.1","3.13.5","3.14.1","3.14.2","3.14.4","3.14.5"), "Level 1: Foundational","Level 2: Advanced")
Level =
ifComplianceControl in ("3.1.1", "3.1.2", "3.1.20", "3.1.22", "3.4.1", "3.5.2", "3.5.2", "3.8.3", "3.13.1", "3.13.5", "3.14.1", "3.14.2", "3.14.4", "3.14.5")"Level 1: Foundational"
else"Level 2: Advanced"

Stage 4: where

| where Level == "Level 1: Foundational"

Stage 5: summarize

| summarize arg_max(TimeGenerated, *) by RecommendationName, AssessedResourceId, Level

Stage 6: summarize

| summarize Failed=countif(State=="Failed"),Passed=countif(State=="Passed"),Total=countif(State=="Passed" or State == "Failed") by Level

Stage 7: extend

| extend PassedControlsPercentage = (Passed/todouble(Total))*100

Stage 8: where

| where PassedControlsPercentage < 70

Stage 9: extend

| extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')

Stage 10: project

| project Level, Total, PassedControlsPercentage, Passed, Failed, RemediationLink, LastObserved=now()

Stage 11: extend

| extend URLCustomEntity = RemediationLink

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Failedproject
LastObservedproject
Levelproject
Passedproject
PassedControlsPercentageproject
RemediationLinkproject
Totalproject
URLCustomEntityextend

Cortex XDR Incident - Medium

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

'A new incident was created in the Cortex XDR portal with a severity "Medium". Click on the events for incident details. '

Rule Body

id: 2e638f3d-611f-4281-81b1-1fd4aa240ffb
name: Cortex XDR Incident - Medium
description: |
  'A new incident was created in the Cortex XDR portal with a severity "Medium". Click on the events for incident details. '
severity: Medium
requiredDataConnectors:
  - connectorId: CortexXDRDataConnector
    dataTypes:
      - PaloAltoCortexXDR_Incidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride: null
customDetails: null
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Users_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: XdrUrl
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hosts_s
sentinelEntitiesMappings: null
query: |
  PaloAltoCortexXDR_Incidents
  | where Severity contains "medium"
  | where Status !contains "resolved_auto"
  // the below line filters for incidents that has at least one user or one host in it. Comment the below line if you want to generated incidents that have 0 user or 0 host as well
  | where HostCount > 0 or UserCount > 0
  | extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)
  | project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

PaloAltoCortexXDR_Incidents

Stage 2: where

| where Severity contains "medium"

Stage 3: where

| where Status !contains "resolved_auto"

Stage 4: where

| where HostCount > 0 or UserCount > 0

Stage 5: extend

| extend Hosts_s = tostring(Hosts), Users_s = tostring(Users)

Stage 6: project

| project TimeGenerated, IncidentId, Status, Severity, IncidentSources, Description, XdrUrl, Hosts_s, Users_s, AlertCategories, HostCount, UserCount, AlertCount, CreationTime, ModificationTime

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Statuscontainsresolved_autoexcludes:Status field:"Status" value:"resolved_auto"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertCategoriesproject
AlertCountproject
CreationTimeproject
Descriptionproject
HostCountproject
Hosts_sproject
IncidentIdproject
IncidentSourcesproject
ModificationTimeproject
Severityproject
Statusproject
TimeGeneratedproject
UserCountproject
Users_sproject
XdrUrlproject

Credential Dumping Tools - File Artifacts

#
Status
available
Severity
high
Time window
1h
Group by
Computer, EventID, Image, ProcessGuid, TargetFilename
Source
github.com/Azure/Azure-Sentinel

This query detects the creation of credential dumping tools files. Several credential dumping tools export files with hardcoded file names. Ref: https://jpcertcc.github.io/ToolAnalysisResultSheet/

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 11: FileCreate

Rule Body

id: 32ffb19e-8ed8-40ed-87a0-1adb4746b7c4
name: Credential Dumping Tools - File Artifacts
description: |
   'This query detects the creation of credential dumping tools files. Several credential dumping tools export files with hardcoded file names.
   Ref: https://jpcertcc.github.io/ToolAnalysisResultSheet/'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - Event
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - Event
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1003.001
query: |
  // Enter a reference list of malicious file artifacts
  let MaliciousFileArtifacts = dynamic (["lsass.dmp","test.pwd","lsremora.dll","lsremora64.dll","fgexec.exe","pwdump","kirbi","wce_ccache","wce_krbtkts","wceaux.dll","PwHashes","SAM.out","SECURITY.out","SYSTEM.out","NTDS.out" "DumpExt.dll","DumpSvc.exe","cachedump64.exe","cachedump.exe","pstgdump.exe","servpw64.exe","servpw.exe","pwdump.exe","fgdump-log"]);
  Event
  | where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID==11
  | parse EventData with * 'TargetFilename">' TargetFilename "<" *
  | where TargetFilename has_any (MaliciousFileArtifacts)
  | parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" *
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, Image, ProcessGuid, TargetFilename
  | extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')
entityMappings:
- entityType: File
  fieldMappings:
    - identifier: Name
      columnName: TargetFilename
- entityType: Host
  fieldMappings:
    - identifier: HostName
      columnName: HostName
    - identifier: DnsDomain
      columnName: DnsDomain
- entityType: Process
  fieldMappings:
    - identifier: CommandLine
      columnName: Image
version: 1.0.3
kind: Scheduled

Stages and Predicates

Let Binding: MaliciousFileArtifacts used in Stage 4

let MaliciousFileArtifacts = dynamic (["lsass.dmp","test.pwd","lsremora.dll","lsremora64.dll","fgexec.exe","pwdump","kirbi","wce_ccache","wce_krbtkts","wceaux.dll","PwHashes","SAM.out","SECURITY.out","SYSTEM.out","NTDS.out" "DumpExt.dll","DumpSvc.exe","cachedump64.exe","cachedump.exe","pstgdump.exe","servpw64.exe","servpw.exe","pwdump.exe","fgdump-log"]);

Stage 1: source

Event

Stage 2: where

| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID==11

Stage 3: parse

| parse EventData with * 'TargetFilename">' TargetFilename "<" *

Stage 4: where

| where TargetFilename has_any (MaliciousFileArtifacts)

Stage 5: parse

| parse EventData with * 'ProcessGuid">' ProcessGuid "<" * 'Image">' Image "<" *

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, Image, ProcessGuid, TargetFilename

Stage 7: extend

| extend HostName = split(Computer, '.', 0)[0], DnsDomain = strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 11 corpus 2 (kusto 2)
field:"EventID" kind:eq value:"11"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 3 (kusto 3)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
TargetFilenamematch
  • DumpSvc.exe transforms: term
  • NTDS.outDumpExt.dll transforms: term
  • PwHashes transforms: term
  • SAM.out transforms: term
  • SECURITY.out transforms: term
  • SYSTEM.out transforms: term
  • cachedump.exe transforms: term
  • cachedump64.exe transforms: term
  • fgdump-log transforms: term
  • fgexec.exe transforms: term
  • kirbi transforms: term
  • lsass.dmp transforms: term
  • lsremora.dll transforms: term
  • lsremora64.dll transforms: term
  • pstgdump.exe transforms: term
  • pwdump transforms: term
  • pwdump.exe transforms: term
  • servpw.exe transforms: term
  • servpw64.exe transforms: term
  • test.pwd transforms: term
  • wce_ccache transforms: term
  • wce_krbtkts transforms: term
  • wceaux.dll transforms: term
field:"TargetFilename" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
EndTimesummarize
EventIDsummarize
Imagesummarize
ProcessGuidsummarize
StartTimesummarize
TargetFilenamesummarize
DnsDomainextend
HostNameextend

Dataverse - Bulk record ownership re-assignment or sharing

#
Status
available
Severity
medium
Time window
1d
Group by
ClientIp, InstanceUrl, Message, UserId
Source
github.com/Azure/Azure-Sentinel

Identifies individual record ownership changes including sharing of records with other users/teams or re-assignment of ownership exceeding a pre-defined threshold.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rule Body

id: 6e480329-84bc-409a-b97b-22e8102af3ca
kind: Scheduled
name: Dataverse - Bulk record ownership re-assignment or sharing
description: Identifies individual record ownership changes including sharing of records
  with other users/teams or re-assignment of ownership exceeding a pre-defined threshold.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1548
query: |
  // Set threshold for number of shared/assigned records
  let detection_threshold = 100;
  let query_frequency = 1h;
  DataverseActivity
  | where TimeGenerated >= ago(query_frequency)
  | where Message in ("ModifyAccess", "Assign", "GrantAccess")
  | summarize
      FirstEvent = min(TimeGenerated),
      LastEvent = max(TimeGenerated),
      Events = count()
      by UserId, Message, InstanceUrl, ClientIp
  | where Events > detection_threshold
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstEvent,
      LastEvent,
      Message,
      Events,
      UserId,
      ClientIp,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: Dataverse - High number of record access modification events
    detected
  alertDescriptionFormat: '{{Events}} events of type {{Message}} detected in {{InstanceUrl}}
    could indicate suspicious or malicious activity.'
version: 3.2.0

Stages and Predicates

Parameters

let detection_threshold = 100;
let query_frequency = 1h;

Stage 1: source

DataverseActivity

Stage 2: where

| where TimeGenerated >= ago(query_frequency)

Stage 3: where

| where Message in ("ModifyAccess", "Assign", "GrantAccess")

Stage 4: summarize

| summarize
    FirstEvent = min(TimeGenerated),
    LastEvent = max(TimeGenerated),
    Events = count()
    by UserId, Message, InstanceUrl, ClientIp

Stage 5: where

| where Events > detection_threshold

Stage 6: extend

| extend
    CloudAppId = int(32780),
    AccountName = tostring(split(UserId, '@')[0]),
    UPNSuffix = tostring(split(UserId, '@')[1])

Stage 7: project

| project
    FirstEvent,
    LastEvent,
    Message,
    Events,
    UserId,
    ClientIp,
    InstanceUrl,
    CloudAppId,
    AccountName,
    UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Eventsgt
  • 100 transforms: number
field:"Events" kind:gt value:"100"
Messagein
  • Assign
  • GrantAccess
  • ModifyAccess
field:"Message" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
Eventsproject
FirstEventproject
InstanceUrlproject
LastEventproject
Messageproject
UPNSuffixproject
UserIdproject

DDoS attack detected

#
Status
available
Severity
high
Time window
1d
Group by
SourceIp, ThreatCategory
Source
github.com/Azure/Azure-Sentinel

Identifies DDoS attack in Azure Firewall IDPS logs.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: c3ffdbe6-2e62-4984-9e80-933ed90b2f6a
name: DDoS attack detected
description: |
  Identifies DDoS attack in Azure Firewall IDPS logs.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureFirewall
    dataTypes:
      - AZFWIdpsSignature
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
query: |
  let TimeWindow   = 90d;    
  let HitThreshold = 10;
  let MinSeverity  = 2;
  let EnableCategoryFilter    = true;
  let EnableDescriptionFilter = false;
  let EnableActionFilter      = false;
  let CategoriesOfInterest = dynamic([
      "Attempted Denial of Service",
      "Denial of Service",
      "Detection of a Denial of Service Attack"
  ]);
  let DescriptionsOfInterest = dynamic([
      "attempted-dos",
      "successful-dos",
      "denial-of-service"
  ]);
  let MatchActions = dynamic(["Deny", "alert"]);
  AZFWIdpsSignature
  | where TimeGenerated >= ago(TimeWindow)
  | where Severity >= MinSeverity
  | where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))
  | where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))
  | where (EnableActionFilter == false) or (Action in~ (MatchActions))
  | summarize
      StartTime   = min(TimeGenerated),
      EndTime     = max(TimeGenerated),
      TotalHits   = count(),
      MaxSeverity = max(Severity),
      Actions     = make_set(Action, 5),
      Signatures  = make_set(SignatureId, 20),
      Description = make_set(substring(tostring(Description), 0, 120), 3)
      by SourceIp, ThreatCategory = Category
  | where TotalHits >= HitThreshold
  | project
      StartTime,
      EndTime,
      SourceIp,
      ThreatCategory,
      TotalHits,
      MaxSeverity,
      Actions,
      Signatures,
      Description
  | order by MaxSeverity desc, TotalHits desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let TimeWindow = 90d;
let HitThreshold = 10;
let MinSeverity = 2;
let EnableCategoryFilter = true;
let EnableDescriptionFilter = false;
let EnableActionFilter = false;
let MatchActions = dynamic(["Deny", "alert"]);

Let Binding: CategoriesOfInterest used in Stage 4

let CategoriesOfInterest = dynamic([
    "Attempted Denial of Service",
    "Denial of Service",
    "Detection of a Denial of Service Attack"
]);

Let Binding: DescriptionsOfInterest used in Stage 5

let DescriptionsOfInterest = dynamic([
    "attempted-dos",
    "successful-dos",
    "denial-of-service"
]);

Stage 1: source

AZFWIdpsSignature

Stage 2: where

| where TimeGenerated >= ago(TimeWindow)

Stage 3: where

| where Severity >= MinSeverity

Stage 4: where

| where (EnableCategoryFilter == false) or (Category has_any (CategoriesOfInterest))

Stage 5: where

| where (EnableDescriptionFilter == false) or (Description has_any (DescriptionsOfInterest))

Stage 6: where

| where (EnableActionFilter == false) or (Action in~ (MatchActions))

Stage 7: summarize

| summarize
    StartTime   = min(TimeGenerated),
    EndTime     = max(TimeGenerated),
    TotalHits   = count(),
    MaxSeverity = max(Severity),
    Actions     = make_set(Action, 5),
    Signatures  = make_set(SignatureId, 20),
    Description = make_set(substring(tostring(Description), 0, 120), 3)
    by SourceIp, ThreatCategory = Category

Stage 8: where

| where TotalHits >= HitThreshold

Stage 9: project

| project
    StartTime,
    EndTime,
    SourceIp,
    ThreatCategory,
    TotalHits,
    MaxSeverity,
    Actions,
    Signatures,
    Description

Stage 10: sort

| order by MaxSeverity desc, TotalHits desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categorymatch
  • Attempted Denial of Service transforms: term
  • Denial of Service transforms: term
  • Detection of a Denial of Service Attack transforms: term
field:"Category" kind:match
Severityge
  • 2 transforms: number
field:"Severity" kind:ge value:"2"
TotalHitsge
  • 10 transforms: number
field:"TotalHits" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionsproject
Descriptionproject
EndTimeproject
MaxSeverityproject
Signaturesproject
SourceIpproject
StartTimeproject
ThreatCategoryproject
TotalHitsproject

GitLab - Abnormal number of repositories deleted

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

'This hunting queries identify an unusual increase of repo deletion activities adversaries may want to disrupt availability or compromise integrity by deleting business data.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 3efd09bd-a582-4410-b7ec-5ff21cfad7bd
name: GitLab - Abnormal number of repositories deleted
description: |
  'This hunting queries identify an unusual increase of repo deletion activities adversaries may want to disrupt availability or compromise integrity by deleting business data.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
query: |
  let LearningPeriod = 7d;
  let BinTime = 1h;
  let RunTime = 1h;
  let StartTime = 1h;
  let NumberOfStds = 3;
  let MinThreshold = 10.0;
  let EndRunTime = StartTime - RunTime;
  let EndLearningTime = StartTime + LearningPeriod;
  let GitLabRepositoryDestroyEvents = (GitLabAudit
  | where RemoveAction == "project" or RemoveAction == "repository");
  GitLabRepositoryDestroyEvents
  | where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))
  | summarize count() by bin(TimeGenerated, BinTime)
  | summarize AvgInLearning = avg(count_), StdInLearning = stdev(count_)
  | extend LearningThreshold = max_of(AvgInLearning + StdInLearning * NumberOfStds, MinThreshold)
  | extend Dummy = 1
  | join kind=innerunique (GitLabRepositoryDestroyEvents
  | where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
  | summarize CountInRunTime = count() by bin(TimeGenerated, BinTime)
  | extend Dummy = 1) on Dummy
  | project-away Dummy
  | where CountInRunTime > LearningThreshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AuthorName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let LearningPeriod = 7d;
let BinTime = 1h;
let RunTime = 1h;
let StartTime = 1h;
let NumberOfStds = 3;
let MinThreshold = 10.0;
let EndRunTime = StartTime - RunTime;
let EndLearningTime = StartTime + LearningPeriod;

let GitLabRepositoryDestroyEvents is inlined into the numbered stages below.

Stages 1 to 2 define let GitLabRepositoryDestroyEvents as the rule's main pipeline source. Stages 3 to 10 use it.

Stage 1: source

GitLabAudit

Stage 2: where

| where RemoveAction == "project" or RemoveAction == "repository"

Stage 3: where

GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(EndLearningTime) .. ago(StartTime))

Stage 4: summarize

| summarize count() by bin(TimeGenerated, BinTime)

Stage 5: summarize

| summarize AvgInLearning = avg(count_), StdInLearning = stdev(count_)

Stage 6: extend

| extend LearningThreshold = max_of(AvgInLearning + StdInLearning * NumberOfStds, MinThreshold)

Stage 7: extend

| extend Dummy = 1

Stage 8: join

| join kind=innerunique (GitLabRepositoryDestroyEvents
| where TimeGenerated between (ago(StartTime) .. ago(EndRunTime))
| summarize CountInRunTime = count() by bin(TimeGenerated, BinTime)
| extend Dummy = 1) on Dummy

Stage 9: project-away

project-away Dummy

Stage 10: where

where CountInRunTime > LearningThreshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
RemoveActioneq
  • project
  • repository
field:"RemoveAction" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvgInLearningsummarize
StdInLearningsummarize
LearningThresholdextend

GWorkspace - Possible maldoc file name in Google drive

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

'Detects possible maldoc file name in Google drive.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: d80d02a8-5da6-11ec-bf63-0242ac130002
name: GWorkspace - Possible maldoc file name in Google drive
description: |
  'Detects possible maldoc file name in Google drive.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: GoogleWorkspaceReportsAPI
    dataTypes:
      - GWorkspaceActivityReports
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  GWorkspaceActivityReports
  | where isnotempty(DocTitle)
  | where DocTitle contains "invoice" or DocTitle contains "payment" or DocTitle contains "order" or DocTitle contains "fax" or DocTitle contains "scan" or DocTitle contains "transfer" or DocTitle contains "report" or DocTitle contains "bill"
  | extend FileCustomEntity = DocTitle, AccountCustomEntity = ActorEmail
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

GWorkspaceActivityReports

Stage 2: where

| where isnotempty(DocTitle)

Stage 3: where

| where DocTitle contains "invoice" or DocTitle contains "payment" or DocTitle contains "order" or DocTitle contains "fax" or DocTitle contains "scan" or DocTitle contains "transfer" or DocTitle contains "report" or DocTitle contains "bill"

Stage 4: extend

| extend FileCustomEntity = DocTitle, AccountCustomEntity = ActorEmail

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DocTitlecontains
  • bill
  • fax
  • invoice
  • order
  • payment
  • report
  • scan
  • transfer
field:"DocTitle" kind:contains
DocTitleis_not_null
  • (no value, null check)
field:"DocTitle" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
FileCustomEntityextend

Hunt domains with Seamless SSO enabled in Entra ID Connect

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

With below KQL query you can search through the IdentityLogon events of Microsoft Defender for Identity to find users and devices still using Seamless SSO in Entra ID Connect. This feature has been marked by the community multiple times as a security risk, and should be disabled if not in use. The KQL query returns the domains where Seamless SSO is enabled, allong with the related users and devices. On top of that, devices get enriched to find their OS distribution, version, and join type and tells you if Seamless SSO is expected to be used for the related device or not. If there are no results or if all results are showing 'No' for the 'Seamless SSO Expected' column, it should be save to disable the feature in Entra ID connect. !Important: This query relies on the Domain Controller EventID 4769 and Defender for Identity. Make sure the EventID is being logged and Defender for Identity is healthy. For more information see references!

References

Telemetry Coverage

Rule Body

// Get all device info we can find
let devices = (
    DeviceInfo
    // Search for 14 days
    | where TimeGenerated > ago(14d)
    // Normalize DeviceName 
    // --> if it is an IP Address we keep it
    // --> If it is not an IP Address we only use the hostname for correlation
    | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
    // Only get interesting data
    | distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);
IdentityLogonEvents
// Get the last 30 days of logon events on Domain Controllers
| where TimeGenerated > ago(30d)
// Search for Seamless SSO events
| where Application == "Active Directory" and Protocol == "Kerberos"
| where TargetDeviceName == "AZUREADSSOACC"
// Save the domain name of the Domain Controller
| extend OnPremisesDomainName = strcat(split(DestinationDeviceName, ".")[-2], ".", split(DestinationDeviceName, ".")[-1])
// Normalize DeviceName 
// --> if it is an IP Address we keep it
// --> If it is not an IP Address we only use the hostname for correlation
| extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
// Only use interesting data and find more info regarding the source device
| distinct AccountUpn, OnPremisesDomainName, DeviceName
| join kind=leftouter devices on DeviceName 
| project-away DeviceName1
// Check if Seamless SSO usage is expected
| extend ['Seamless SSO Expected'] = case(
    // Cases where we do not expect Seamless SSO to be used
    JoinType == "Hybrid Azure AD Join" or 
    JoinType == "AAD Joined" or
    JoinType == "AAD Registered", "No",
    // Cases where we do expect Seamless SSO to be used
    JoinType == "Domain Joined" or 
    (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0) , "Yes", 
    // Cases that need to be verified
    "Unknown (to verify)"
)

Stages and Predicates

Let Binding: devices used in Stages 1, 9

let devices = (
    DeviceInfo
    | where TimeGenerated > ago(14d)
    | extend DeviceName = iff(ipv4_is_private(DeviceName), DeviceName, tolower(split(DeviceName, ".")[0]))
    | distinct DeviceName, OSPlatform, OSVersion, DeviceId, OnboardingStatus, Model, JoinType
);

Stage 1: source

let devices

Stage 2: source

IdentityLogonEvents

Stage 3: where

where TimeGenerated > ago(2592000s)

Stage 4: where

where Application == "Active Directory" and Protocol == "Kerberos"

Stage 5: where

where TargetDeviceName == "AZUREADSSOACC"

Stage 6: extend

extend OnPremisesDomainName

Stage 7: extend

extend DeviceName
DeviceName =
if(ipv4_is_in_range(DeviceName, "10.0.0.0/8") or ipv4_is_in_range(DeviceName, "172.16.0.0/12") or ipv4_is_in_range(DeviceName, "192.168.0.0/16") or ipv4_is_in_range(DeviceName, "169.254.0.0/16") or ipv4_is_in_range(DeviceName, "127.0.0.0/8"))DeviceName
elsetolower(split(DeviceName, ".")[0])

Stage 8: distinct

distinct AccountUpn, DeviceName, OnPremisesDomainName

Stage 9: join

join kind=leftouter (devices) on DeviceName

Stage 10: project-away

project-away DeviceName1

Stage 11: extend

extend Seamless SSO Expected
Seamless SSO Expected =
if((JoinType == "Hybrid Azure AD Join" or JoinType == "AAD Joined") or JoinType == "AAD Registered")"No"
elif(JoinType == "Domain Joined" or (OSPlatform startswith "Windows" and toreal(OSVersion) < 10.0))"Yes"
else"Unknown (to verify)"

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
OnPremisesDomainNameextend
DeviceNameextend
Seamless SSO Expectedextend

Lookout - High Severity Mobile Threats Detected (v2)

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

'Detects high severity mobile threats from Lookout Mobile Risk API v2 with enhanced threat intelligence and device context. This rule leverages the comprehensive v2 field set to provide detailed threat classification, risk assessment, and device compliance status for improved security monitoring.'

MITRE ATT&CK Coverage

Rule Body

id: 8b4a5c7e-2f91-4d8a-9e3b-1c6f8a2d4e9f
name: Lookout - High Severity Mobile Threats Detected (v2)
description: |
  'Detects high severity mobile threats from Lookout Mobile Risk API v2 with enhanced threat intelligence and device context. This rule leverages the comprehensive v2 field set to provide detailed threat classification, risk assessment, and device compliance status for improved security monitoring.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: LookoutAPI
    dataTypes:
      - LookoutEvents
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
  - DefenseEvasion
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1424
  - T1418
  - T1629
  - T1630
query: |
  LookoutEvents
  | where EventType == "THREAT"
  | where ThreatSeverity in ("CRITICAL", "HIGH")
  | where ThreatStatus in ("OPEN", "ACTIVE")
  | where ThreatAction == "DETECTED"
  | extend
      ThreatRiskScore = case(
          ThreatSeverity == "CRITICAL", 10,
          ThreatSeverity == "HIGH", 8,
          ThreatSeverity == "MEDIUM", 5,
          ThreatSeverity == "LOW", 2,
          1
      ),
      DeviceRiskLevel = case(
          DeviceSecurityStatus == "THREATS_HIGH", "High",
          DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
          DeviceSecurityStatus == "THREATS_LOW", "Low",
          "Unknown"
      ),
      ThreatCategory = case(
          ThreatClassifications has "MALWARE", "Malware",
          ThreatClassifications has "PHISHING", "Phishing", 
          ThreatClassifications has "SPYWARE", "Spyware",
          ThreatClassifications has "TROJAN", "Trojan",
          ThreatClassifications has "ADWARE", "Adware",
          "Other"
      )
  | extend ComplianceImpact = case(
      DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8, "Critical",
      DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5, "High", 
      DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8, "High",
      DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5, "Medium",
      "Low"
  )
  | project
      TimeGenerated,
      EventId,
      ThreatId,
      ThreatType,
      ThreatSeverity,
      ThreatRiskScore,
      ThreatCategory,
      ThreatClassifications,
      ThreatStatus,
      ThreatDescription,
      ThreatApplicationName,
      ThreatPackageName,
      ThreatPackageSha,
      DeviceGuid,
      DevicePlatform,
      DeviceOSVersion,
      DeviceManufacturer,
      DeviceModel,
      DeviceEmailAddress,
      DeviceSecurityStatus,
      DeviceRiskLevel,
      DeviceComplianceStatus,
      ComplianceImpact,
      ClientLookoutSDKVersion,
      MDMConnectorId,
      MDMExternalId,
      TargetEmailAddress,
      TargetPlatform,
      ActorType,
      ActorGuid
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeviceEmailAddress
      - identifier: Name
        columnName: TargetEmailAddress
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceGuid
      - identifier: OSFamily
        columnName: DevicePlatform
      - identifier: OSVersion
        columnName: DeviceOSVersion
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: ThreatApplicationName
      - identifier: Value
        columnName: ThreatPackageSha
customDetails:
  ThreatType: ThreatType
  ThreatSeverity: ThreatSeverity
  ThreatRiskScore: ThreatRiskScore
  ThreatCategory: ThreatCategory
  ThreatClasses: ThreatClassifications
  ThreatStatus: ThreatStatus
  DevicePlatform: DevicePlatform
  DeviceSecStatus: DeviceSecurityStatus
  DeviceRiskLevel: DeviceRiskLevel
  ComplianceImpact: ComplianceImpact
  MDMConnectorId: MDMConnectorId
alertDetailsOverride:
  alertDisplayNameFormat: "High Severity Mobile Threat: {{ThreatType}} on {{DevicePlatform}} Device"
  alertDescriptionFormat: "{{ThreatSeverity}} {{ThreatCategory}} threat on {{DevicePlatform}}"
  alertTacticsColumnName: ThreatCategory
  alertSeverityColumnName: ThreatSeverity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
      - Host
    groupByAlertDetails:
      - ThreatType
      - DeviceGuid
    groupByCustomDetails:
      - ThreatCategory
      - DevicePlatform
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 2.0.5
kind: Scheduled

Stages and Predicates

Stage 1: source

LookoutEvents

Stage 2: where

| where EventType == "THREAT"

Stage 3: where

| where ThreatSeverity in ("CRITICAL", "HIGH")

Stage 4: where

| where ThreatStatus in ("OPEN", "ACTIVE")

Stage 5: where

| where ThreatAction == "DETECTED"

Stage 6: extend

| extend
    ThreatRiskScore = case(
        ThreatSeverity == "CRITICAL", 10,
        ThreatSeverity == "HIGH", 8,
        ThreatSeverity == "MEDIUM", 5,
        ThreatSeverity == "LOW", 2,
        1
    ),
    DeviceRiskLevel = case(
        DeviceSecurityStatus == "THREATS_HIGH", "High",
        DeviceSecurityStatus == "THREATS_MEDIUM", "Medium",
        DeviceSecurityStatus == "THREATS_LOW", "Low",
        "Unknown"
    ),
    ThreatCategory = case(
        ThreatClassifications has "MALWARE", "Malware",
        ThreatClassifications has "PHISHING", "Phishing", 
        ThreatClassifications has "SPYWARE", "Spyware",
        ThreatClassifications has "TROJAN", "Trojan",
        ThreatClassifications has "ADWARE", "Adware",
        "Other"
    )
DeviceRiskLevel =
ifDeviceSecurityStatus == "THREATS_HIGH""High"
elifDeviceSecurityStatus == "THREATS_MEDIUM""Medium"
elifDeviceSecurityStatus == "THREATS_LOW""Low"
else"Unknown"
ThreatCategory =
ifThreatClassifications has "MALWARE""Malware"
elifThreatClassifications has "PHISHING""Phishing"
elifThreatClassifications has "SPYWARE""Spyware"
elifThreatClassifications has "TROJAN""Trojan"
elifThreatClassifications has "ADWARE""Adware"
else"Other"
ThreatRiskScore =
ifThreatSeverity == "CRITICAL"10
elifThreatSeverity == "HIGH"8
elifThreatSeverity == "MEDIUM"5
elifThreatSeverity == "LOW"2
else1

Stage 7: extend

| extend ComplianceImpact = case(
    DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8, "Critical",
    DeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5, "High", 
    DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8, "High",
    DeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5, "Medium",
    "Low"
)
ComplianceImpact =
ifDeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 8"Critical"
elifDeviceComplianceStatus == "Non-Compliant" and ThreatRiskScore >= 5"High"
elifDeviceComplianceStatus == "Partial" and ThreatRiskScore >= 8"High"
elifDeviceComplianceStatus == "Partial" and ThreatRiskScore >= 5"Medium"
else"Low"

Stage 8: project

| project
    TimeGenerated,
    EventId,
    ThreatId,
    ThreatType,
    ThreatSeverity,
    ThreatRiskScore,
    ThreatCategory,
    ThreatClassifications,
    ThreatStatus,
    ThreatDescription,
    ThreatApplicationName,
    ThreatPackageName,
    ThreatPackageSha,
    DeviceGuid,
    DevicePlatform,
    DeviceOSVersion,
    DeviceManufacturer,
    DeviceModel,
    DeviceEmailAddress,
    DeviceSecurityStatus,
    DeviceRiskLevel,
    DeviceComplianceStatus,
    ComplianceImpact,
    ClientLookoutSDKVersion,
    MDMConnectorId,
    MDMExternalId,
    TargetEmailAddress,
    TargetPlatform,
    ActorType,
    ActorGuid

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorGuidproject
ActorTypeproject
ClientLookoutSDKVersionproject
ComplianceImpactproject
DeviceComplianceStatusproject
DeviceEmailAddressproject
DeviceGuidproject
DeviceManufacturerproject
DeviceModelproject
DeviceOSVersionproject
DevicePlatformproject
DeviceRiskLevelproject
DeviceSecurityStatusproject
EventIdproject
MDMConnectorIdproject
MDMExternalIdproject
TargetEmailAddressproject
TargetPlatformproject
ThreatApplicationNameproject
ThreatCategoryproject
ThreatClassificationsproject
ThreatDescriptionproject
ThreatIdproject
ThreatPackageNameproject
ThreatPackageShaproject
ThreatRiskScoreproject
ThreatSeverityproject
ThreatStatusproject
ThreatTypeproject
TimeGeneratedproject

LSASS Dumping using Debug Privileges

#
Group by
DeviceId, InitiatingProcessId, InitiatingProcessSHA1
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query searches for a process that requests the SeDebugPrivilege privilege and opens LSASS memory using specific permission 0x1fffff which represents PROCESS_ALL_ACCESS.

Known False Positives

  • There are some applications that perform these actions for legitimate purposes. One example is Procmon by Sysinternals.

MITRE ATT&CK Coverage

TacticTechniques
Execution
Credential Access

References

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20); // Value for SeDebugPrivilege is 2**20 = 0x100000.
let LSASSOpen=materialize (
    DeviceEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "OpenProcessApiCall"
    | where FileName =~ "lsass.exe"
    | extend AccessRights=parse_json(AdditionalFields).DesiredAccess
    | where AccessRights == 0x1fffff // PROCESS_ALL_ACCESS.
    | summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType == "ProcessPrimaryTokenModified"
| where isnotempty(InitiatingProcessSHA1)
// Look for processes that request debug privilege that also opened LSASS
| where InitiatingProcessSHA1 in ((LSASSOpen | project InitiatingProcessSHA1)) // Speeds up the query.
| lookup kind=inner LSASSOpen on DeviceId, InitiatingProcessSHA1, InitiatingProcessId
// Check that debug privilege is enabled.
| extend AdditionalFields=parse_json(AdditionalFields)
| extend CurrentTokenPrivEnabled = toint(AdditionalFields.CurrentTokenPrivEnabled)
| extend OriginalTokenPrivEnabled = toint(AdditionalFields.OriginalTokenPrivEnabled)
// Value for SeDebugPrivilege is 2**20 = 0x100000.
// Refer to https://downloads.volatilityfoundation.org//omfw/2012/OMFW2012_Gurkok.pdf for numeric values for privileges.
| extend DebugPrivCurrent = binary_and(CurrentTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
| extend DebugPrivOrig = binary_and(OriginalTokenPrivEnabled,SeDebugPrivilege) == SeDebugPrivilege
// Check for processes that have debug privilege after the event, but did not have it before.
| where not(DebugPrivOrig) and DebugPrivCurrent
| extend CleanCmdLine = parse_command_line(InitiatingProcessCommandLine, "windows")
| where not(InitiatingProcessFileName =~ "tasklist.exe" and CleanCmdLine has_any ("/m", "-m"))
| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")
| project-reorder Timestamp, DeviceId, InitiatingProcessFileName
// Begin environment-specific filter.
// End environment-specific filter.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let SeDebugPrivilege = binary_shift_left(1, 20);

Let Binding: LSASSOpen used in Stages 1, 6, 7

let LSASSOpen = materialize (
    DeviceEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "OpenProcessApiCall"
    | where FileName =~ "lsass.exe"
    | extend AccessRights=parse_json(AdditionalFields).DesiredAccess
    | where AccessRights == 0x1fffff
    | summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1
);

Stage 1: source

let LSASSOpen

Stage 2: source

DeviceEvents

Stage 3: where

where ...

Stage 4: where

where ActionType == "ProcessPrimaryTokenModified"

Stage 5: where

where isnotempty(InitiatingProcessSHA1)

Stage 6: where

where /* macro: (InitiatingProcessSHA1 in (LSASSOpen | project)) */

Stage 7: kusto:lookup

lookup kind=inner (LSASSOpen) on DeviceId, InitiatingProcessSHA1, InitiatingProcessId

Stage 8: extend (5 consecutive steps)

extend AdditionalFields, CurrentTokenPrivEnabled, DebugPrivCurrent, DebugPrivOrig, OriginalTokenPrivEnabled

Stage 9: where

where not (DebugPrivOrig == true) and DebugPrivCurrent == true

Stage 10: extend

extend CleanCmdLine

Stage 11: where

where not (((CleanCmdLine has "/m" or CleanCmdLine has "-m") and InitiatingProcessFileName =~ "tasklist.exe"))

Stage 12: extend

extend DnsDomain, HostName

Stage 13: project-reorder

project-reorder

Stage 14: summarize aggregation inside the lookup branch

summarize by DeviceId, InitiatingProcessId, InitiatingProcessSHA1

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
InitiatingProcessIdsummarize
InitiatingProcessSHA1summarize

Malware Detection Session Finished

#
Status
available
Severity
informational
Time window
3h
Group by
$left.Result, $right.JobResult
Source
github.com/Azure/Azure-Sentinel

Detects when malware detection session finishes.

Rule Body

id: 88b9223c-29ff-48a9-a745-c553aa0dbae2
name: Malware Detection Session Finished
description: Detects when malware detection session finishes.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "let action_results_lookup = union isfuzzy=true (datatable(JobResult:string,\
  \ JobResultMessage:string)[]), (_GetWatchlist(\"action_results_lookup\")); \nVeeam_GetSecurityEvents\n\
  | where instanceId == 42210\n| extend Result = extract(\"Result=\\\"([^\\\"]*)\\\
  \"\", 1, SyslogMessage)\n| extend SessionID = extract(\"SessionID=\\\"([^\\\"]*)\\\
  \"\", 1, SyslogMessage)\n| lookup kind=leftouter (action_results_lookup)\n    on\
  \ $left.Result == $right.JobResult\n| project\n    Date = format_datetime(TimeGenerated,\
  \ 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n    EventId = instanceId,\n\
  \    [\"Session ID\"] = SessionID,\n    [\"StateMessage\"] = JobResultMessage,\n\
  \    MessageDetails = Description,\n    Severity = SeverityDescription\n"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Parameters

let action_results_lookup = union isfuzzy=true (datatable(JobResult:string, JobResultMessage:string)[]), (_GetWatchlist("action_results_lookup"));

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 42210

Stage 3: extend

| extend Result = extract("Result=\"([^\"]*)\"", 1, SyslogMessage)

Stage 4: extend

| extend SessionID = extract("SessionID=\"([^\"]*)\"", 1, SyslogMessage)

Stage 5: kusto:lookup

lookup kind=leftouter (action_results_lookup) on $left.Result, $right.JobResult

Stage 6: project

project DataSource, Date, EventId, MessageDetails, Session ID, Severity, StateMessage

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Session IDproject
Severityproject
StateMessageproject

Oracle - Put suspicious file

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

'Detects PUT or POST of suspicious file'

MITRE ATT&CK Coverage

Rule Body

id: edc2f2b4-573f-11ec-bf63-0242ac130002
name: Oracle - Put suspicious file
description: |
  'Detects PUT or POST of suspicious file'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1190
  - T1133
  - T1048
query: |
  OracleWebLogicServerEvent
  | where HttpRequestMethod in~ ("POST", "PUT") 
  | extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
  | where isnotempty(File)
  | where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
  | extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

OracleWebLogicServerEvent

Stage 2: where

| where HttpRequestMethod in~ ("POST", "PUT")

Stage 3: extend

| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))

Stage 4: where

| where isnotempty(File)

Stage 5: where

| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"

Stage 6: extend

| extend IPCustomEntity = SrcIpAddr, FileCustomEntity = File, UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Fileis_not_null
  • (no value, null check)
field:"File" kind:is_not_null
Fileregex_match
  • ([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)
field:"File" kind:regex_match value:"([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)"
HttpRequestMethodin
  • POST
  • PUT
field:"HttpRequestMethod" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fileextend
FileCustomEntityextend
IPCustomEntityextend
UrlCustomEntityextend

Stopping multiple processes using taskkill

#
Status
available
Severity
medium
Time window
2m
Group by
DeviceId, DeviceName
Source
github.com/Azure/Azure-Sentinel

This query checks for attempts to stop at least 10 separate processes using the taskkill.exe utility. This is a common technique used by ransomware to stop security products and other processes.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 4dd31bd5-11a3-4b9c-a7c5-4927ab4f2a77
name: Stopping multiple processes using taskkill
description: |
  This query checks for attempts to stop at least 10 separate processes using the taskkill.exe utility. This is a common technique used by ransomware to stop security products and other processes.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  // Find attempts to stop processes using taskkill.exe
  DeviceProcessEvents
  | where FileName =~ "taskkill.exe" 
  | summarize taskKillCount = dcount(ProcessCommandLine), TaskKillList = make_set(ProcessCommandLine, 10000) by DeviceId, DeviceName, bin(TimeGenerated, 2m)
  | where taskKillCount > 10
  | 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
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where FileName =~ "taskkill.exe"

Stage 3: summarize

| summarize taskKillCount = dcount(ProcessCommandLine), TaskKillList = make_set(ProcessCommandLine, 10000) by DeviceId, DeviceName, bin(TimeGenerated, 2m)

Stage 4: where

| where taskKillCount > 10

Stage 5: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 6: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNameeq
  • taskkill.exe corpus 5 (elastic 2, splunk 2, kusto 1)
field:"process_basename" kind:eq value:"taskkill.exe"
taskKillCountgt
  • 10 transforms: number
field:"taskKillCount" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
DeviceNamesummarize
TaskKillListsummarize
taskKillCountsummarize
HostNameextend
DnsDomainextend

Suspicious SPN logon from workstation (DumpGuard)

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

With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. Since the DumpGuard tool needs to use an SPN enabled account (in the POC they use a machine account) for two exploitation scenario's, it is interesting to look for TGT requests happening from client devices for SPN enabled accounts.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

References

Telemetry Coverage

Rule Body

let spn_accounts = toscalar(
    // Search for all SPNs we can find in historic logs
    IdentityLogonEvents
    | where TimeGenerated > ago(14d)
    | where Application == "Active Directory"
    | where isnotempty(AdditionalFields.Spns)
    | extend Spns = split(AdditionalFields.Spns, ",")
    | summarize make_set(Spns)
);
let workstation_subnets = toscalar(
    DeviceNetworkInfo
    | where TimeGenerated > ago(14d)
    // Filter out empty device names
    | where isnotempty(DeviceName)
    // Expand IP Addresses
    | mv-expand todynamic(IPAddresses)
    // Focus on device name and IP Address info
    | distinct DeviceName, tostring(IPAddresses)
    // Filter out IPv6 addresses, /32 addresses, and APIPA addresses
    | where todynamic(IPAddresses).IPAddress !contains ":"
    | where todynamic(IPAddresses).SubnetPrefix != "32"
    | where todynamic(IPAddresses).IPAddress !startswith "169.254"
    // Find Device Type of the device
    | join kind=inner (
        DeviceInfo
        | where TimeGenerated > ago(30d)
        | distinct DeviceName, DeviceType
    ) on DeviceName
    // Only focus on workstations
    | where DeviceType == "Workstation"
    // Create Network Address based on the host IP Address and create a distinct list
    | extend NetworkAddress = format_ipv4_mask(tostring(todynamic(IPAddresses).IPAddress), tolong(todynamic(IPAddresses).SubnetPrefix))
    | summarize make_set(NetworkAddress)
);
IdentityLogonEvents
| where TimeGenerated > ago(1h)
// Get AD TGT requests by looking for Kerberos requests to KRBTGT account
| where Application == "Active Directory"
| where Protocol == "Kerberos"
| where AdditionalFields.Spns contains "krbtgt"
// Check for requests to account names with SPNs
| where AccountName in (spn_accounts)
// Check if IP Address is from a client range
| where ipv4_is_in_any_range(IPAddress, workstation_subnets)
// Optional - Ignore failed logins
| where ActionType != "LogonFailed"

Stages and Predicates

Let Binding: spn_accounts used in Stage 6

let spn_accounts = toscalar(
    IdentityLogonEvents
    | where TimeGenerated > ago(14d)
    | where Application == "Active Directory"
    | where isnotempty(AdditionalFields.Spns)
    | extend Spns = split(AdditionalFields.Spns, ",")
    | summarize make_set(Spns)
);

Let Binding: workstation_subnets used in Stage 7

let workstation_subnets = toscalar(
    DeviceNetworkInfo
    | where TimeGenerated > ago(14d)
    | where isnotempty(DeviceName)
    | mv-expand todynamic(IPAddresses)
    | distinct DeviceName, tostring(IPAddresses)
    | where todynamic(IPAddresses).IPAddress !contains ":"
    | where todynamic(IPAddresses).SubnetPrefix != "32"
    | where todynamic(IPAddresses).IPAddress !startswith "169.254"
    | join kind=inner (
        DeviceInfo
        | where TimeGenerated > ago(30d)
        | distinct DeviceName, DeviceType
    ) on DeviceName
    | where DeviceType == "Workstation"
    | extend NetworkAddress = format_ipv4_mask(tostring(todynamic(IPAddresses).IPAddress), tolong(todynamic(IPAddresses).SubnetPrefix))
    | summarize make_set(NetworkAddress)
);

Stage 1: source

IdentityLogonEvents

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where Application == "Active Directory"

Stage 4: where

| where Protocol == "Kerberos"

Stage 5: where

| where AdditionalFields.Spns contains "krbtgt"

Stage 6: where

| where AccountName in (spn_accounts)

Stage 7: where

| where ipv4_is_in_any_range(IPAddress, workstation_subnets)

Stage 8: where

| where ActionType != "LogonFailed"

Indicators

These rows show field, operator, and value matches.

TI map Domain entity to EmailUrlInfo

#
Severity
medium
Time window
14d
Group by
$left.DomainName, $right.UrlDomain, IndicatorId, NetworkMessageId, Url, UrlDomain
Source
github.com/Azure/Azure-Sentinel

Identifies a match in EmailUrlInfo table from any Domain IOC from TI.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 87cc75df-d7b2-44f1-b064-ee924edfc879
name: TI map Domain entity to EmailUrlInfo
description: |
  'Identifies a match in EmailUrlInfo table from any Domain IOC from TI.'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - EmailUrlInfo
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
    let dt_lookBack = 1h; // Define the lookback period for email data as 1 hour
    let ioc_lookBack = 14d; // Define the lookback period for threat intelligence data as 14 days
    let EmailUrlInfo_ = EmailUrlInfo
    | where isnotempty(Url) or isnotempty(UrlDomain) // Filter for non-empty URLs or URL domains
    | where TimeGenerated >= ago(dt_lookBack) // Filter for records within the lookback period
    | extend Url = tolower(Url), UrlDomain = tolower(UrlDomain) // Convert URLs and domains to lowercase
    | extend EmailUrlInfo_TimeGenerated = TimeGenerated; // Create a new column for the time generated
    let EmailEvents_ = EmailEvents
    | where TimeGenerated >= ago(dt_lookBack); // Filter email events within the lookback period
    let TI_Urls = ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
    | where isnotempty(Url) // Filter for non-empty URLs
    | extend Url = tolower(Url) // Convert URLs to lowercase
    | join kind=innerunique (EmailUrlInfo_) on Url // Join with email URL info on URL
    | where Active == true and ExpirationDateTime > now() // Filter for active indicators that haven't expired
    | where EmailUrlInfo_TimeGenerated < ExpirationDateTime // Ensure email info was generated before the indicator expired
    | summarize EmailUrlInfo_TimeGenerated = arg_max(EmailUrlInfo_TimeGenerated, *) by IndicatorId, Url // Get the latest email info for each indicator
    | project EmailUrlInfo_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Url, UrlLocation, NetworkMessageId; // Select relevant columns
    let TI_Domains = ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack) // Filter threat intelligence indicators within the lookback period
    | where isnotempty(DomainName) // Filter for non-empty domain names
    | extend DomainName = tolower(DomainName) // Convert domain names to lowercase
    | join kind=innerunique (EmailUrlInfo_) on $left.DomainName == $right.UrlDomain // Join with email URL info on domain name
    | where Active == true and ExpirationDateTime > now() // Filter for active indicators that haven't expired
    | where EmailUrlInfo_TimeGenerated < ExpirationDateTime // Ensure email info was generated before the indicator expired
    | summarize EmailUrlInfo_TimeGenerated = arg_max(EmailUrlInfo_TimeGenerated, *) by IndicatorId, UrlDomain // Get the latest email info for each indicator
    | project EmailUrlInfo_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, UrlDomain, UrlLocation, NetworkMessageId; // Select relevant columns
    union TI_Urls, TI_Domains // Combine URL and domain threat intelligence data
    | extend timestamp = EmailUrlInfo_TimeGenerated // Add a timestamp column
    | join kind=inner (EmailEvents_) on NetworkMessageId // Join with email events on network message ID
    | where DeliveryAction !has "Blocked" // Filter out blocked delivery actions
    | extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0]); // Extract name and UPN suffix from recipient email address
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RecipientEmailAddress
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

Let Binding: EmailUrlInfo_

let EmailUrlInfo_ = EmailUrlInfo
| where isnotempty(Url) or isnotempty(UrlDomain)
| where TimeGenerated >= ago(dt_lookBack)
| extend Url = tolower(Url), UrlDomain = tolower(UrlDomain)
| extend EmailUrlInfo_TimeGenerated = TimeGenerated;

Let Binding: EmailEvents_

let EmailEvents_ = EmailEvents
| where TimeGenerated >= ago(dt_lookBack);

Union (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: TI_Urls, TI_Domains

Leg 1: TI_Urls

Leg 2: TI_Domains

Applied to the Combined Result

| extend timestamp = EmailUrlInfo_TimeGenerated | join kind=inner (EmailEvents_) on NetworkMessageId | where DeliveryAction !has "Blocked" | extend Name = tostring(split(RecipientEmailAddress, '@', 0)[0]), UPNSuffix = tostring(split(RecipientEmailAddress, '@', 1)[0]);

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true
field:"Active" kind:eq value:"true"
DomainNameis_not_null
  • (no value, null check)
field:"DomainName" kind:is_not_null
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null
UrlDomainis_not_null
  • (no value, null check)
field:"UrlDomain" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DeliveryActionmatchBlockedexcludes:DeliveryAction field:"DeliveryAction" value:"Blocked"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ConfidenceScoreproject
Descriptionproject
EmailUrlInfo_TimeGeneratedproject
ExpirationDateTimeproject
IndicatorIdproject
NetworkMessageIdproject
ThreatTypeproject
UrlDomainproject
UrlLocationproject
timestampextend
Nameextend
UPNSuffixextend

VMware SD-WAN Edge - Network Anomaly Detection - RPF Check Failure

#
Severity
low
Time window
1h
Source
github.com/Azure/Azure-Sentinel

The VMware SD-WAN Edge appliance received packets that failed a Reverse Path Forwarding (RPF) Check. Reverse path forwarding (RPF) check is a network security mechanism that verifies whether the source IP address of a packet is reachable through the incoming interface on which the packet is received. The packet is dropped if the source IP address is not reachable through the incoming interface. RPF checks prevent spoofing attacks, in which an attacker uses a forged source IP address to make it appear that the packets are coming from a trusted source. This can allow the attacker to gain unauthorized network access or launch a denial-of-service attack against a target system. An IP fragmentation attack is a cyberattack that exploits how IP packets are fragmented and reassembled. IP fragmentation is a process by which large IP packets are broken down into smaller packets to transmit them over networks with smaller Maximum Transmission Unit (MTU) sizes. This analytics rule analyzes Syslog streams; these alerts are not reported by default if Search API is used.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 840b050f-842b-4264-8973-d4f9b65facb5
name: VMware SD-WAN Edge - Network Anomaly Detection - RPF Check Failure
version: 1.0.0
kind: Scheduled
description: |-
  The VMware SD-WAN Edge appliance received packets that failed a Reverse Path Forwarding (RPF) Check.

  Reverse path forwarding (RPF) check is a network security mechanism that verifies whether the source IP address of a packet is reachable through the incoming interface on which the packet is received. The packet is dropped if the source IP address is not reachable through the incoming interface.

  RPF checks prevent spoofing attacks, in which an attacker uses a forged source IP address to make it appear that the packets are coming from a trusted source. This can allow the attacker to gain unauthorized network access or launch a denial-of-service attack against a target system.

  An IP fragmentation attack is a cyberattack that exploits how IP packets are fragmented and reassembled. IP fragmentation is a process by which large IP packets are broken down into smaller packets to transmit them over networks with smaller Maximum Transmission Unit (MTU) sizes.

  This analytics rule analyzes Syslog streams; these alerts are not reported by default if Search API is used.
severity: Low
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
requiredDataConnectors:
  - connectorId: VMwareSDWAN
    dataTypes:
      - SDWAN
tactics:
- Impact
relevantTechniques:
- T1498
query: |+
  Syslog
  | where SyslogMessage contains "VCF Drop"
  | where SyslogMessage contains "Reverse path forwarding check fail"
  | project-rename EdgeName=HostName
  | project-away Computer, HostIP, SourceSystem, Type
  | extend OverlaySegmentName = extract("SEGMENT_NAME=(.+) COUNT=", 1, SyslogMessage)
  | extend IpProtocol = extract("PROTO=(.+) SRC=", 1, SyslogMessage)
  | extend SrcIpAddress = extract("SRC=(.+) DST=", 1, SyslogMessage)
  | extend DstIpAddress = extract("DST=(.+) REASON=", 1, SyslogMessage)
  | extend EdgeFwAction = extract("ACTION=(.+) SEGMENT=", 1, SyslogMessage)
  | extend SyslogTag = extract("^(.+): ACTION=", 1, SyslogMessage)
  | extend pcktCount = extract("COUNT=([0-9]+)$", 1, SyslogMessage)
  | project
      TimeGenerated,
      EdgeFwAction,
      EdgeName,
      SrcIpAddress,
      IpProtocol,
      DstIpAddress,
      pcktCount,
      SyslogTag

suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails:
  Edge_Name: EdgeName
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddress
suppressionDuration: 5h

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where SyslogMessage contains "VCF Drop"

Stage 3: where

| where SyslogMessage contains "Reverse path forwarding check fail"

Stage 4: project-rename

| project-rename EdgeName=HostName

Stage 5: project-away

| project-away Computer, HostIP, SourceSystem, Type

Stage 6: extend (7 consecutive steps)

| extend OverlaySegmentName = extract("SEGMENT_NAME=(.+) COUNT=", 1, SyslogMessage)
| extend IpProtocol = extract("PROTO=(.+) SRC=", 1, SyslogMessage)
| extend SrcIpAddress = extract("SRC=(.+) DST=", 1, SyslogMessage)
| extend DstIpAddress = extract("DST=(.+) REASON=", 1, SyslogMessage)
| extend EdgeFwAction = extract("ACTION=(.+) SEGMENT=", 1, SyslogMessage)
| extend SyslogTag = extract("^(.+): ACTION=", 1, SyslogMessage)
| extend pcktCount = extract("COUNT=([0-9]+)$", 1, SyslogMessage)

Stage 7: project

| project
    TimeGenerated,
    EdgeFwAction,
    EdgeName,
    SrcIpAddress,
    IpProtocol,
    DstIpAddress,
    pcktCount,
    SyslogTag

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagecontains
  • Reverse path forwarding check fail
  • VCF Drop
field:"SyslogMessage" kind:contains

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstIpAddressproject
EdgeFwActionproject
EdgeNameproject
IpProtocolproject
SrcIpAddressproject
SyslogTagproject
TimeGeneratedproject
pcktCountproject

Back to all Kusto rules