Detection rules › Kusto

Kusto rules: 0v

BloodHound Attack Path Finding - Large Default Groups With RDP Access

#

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 - Large Default Groups With RDP Access

Rule Body

id: f4776f0d-86b7-470a-993a-f3152bbb8917
name: BloodHound Attack Path Finding - Large Default Groups With RDP Access
description: BloodHound Attack Path Finding - Large Default Groups With RDP Access
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "LargeDefaultGroupsRDP"
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 == "LargeDefaultGroupsRDP"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • LargeDefaultGroupsRDP
field:"Finding" kind:eq value:"LargeDefaultGroupsRDP"

Cisco SEG - Potential phishing link

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

'Detects mails with suspicious links.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2e5158e1-9fc2-40ff-a909-c701a13a0405
name: Cisco SEG - Potential phishing link
description: |
  'Detects mails with suspicious links.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dl_cat = dynamic(['Illegal Downloads']);
  CiscoSEGEvent
  | where NetworkDirection =~ 'Incoming'
  | where tostring(AdditionalFields) has 'ESAURLDetails'
  | extend link_cat = extract(@"'Category': '(.*?)'", 1, tostring(AdditionalFields))
  | where link_cat in~ (dl_cat)
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let dl_cat = dynamic(['Illegal Downloads']);

Stage 1: source

CiscoSEGEvent

Stage 2: where

| where NetworkDirection =~ 'Incoming'

Stage 3: where

| where tostring(AdditionalFields) has 'ESAURLDetails'

Stage 4: extend

| extend link_cat = extract(@"'Category': '(.*?)'", 1, tostring(AdditionalFields))

Stage 5: where

| where link_cat in~ (dl_cat)

Stage 6: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
link_catextend
AccountCustomEntityextend

Contrast ADR - Security Incident Alert

#

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

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

'Monitors Contrast ADR security incidents across all applications and environments. This rule creates alerts for all security incidents detected by Contrast Security ADR, providing comprehensive visibility into application security events requiring investigation.'

MITRE ATT&CK Coverage

Rule Body

id: 7ce5956f-48f2-42f5-8e2e-c254e7643c11
name: Contrast ADR - Security Incident Alert
description: |
  'Monitors Contrast ADR security incidents across all applications and environments. This rule creates alerts for all security incidents detected by Contrast Security ADR, providing comprehensive visibility into application security events requiring investigation.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ContrastADRCCF
    dataTypes:
      - ContrastADRIncidents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - DefenseEvasion
  - Discovery
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1055
  - T1018
  - T1008
query: |
 ContrastADRIncidents_CL
entityMappings:
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: ObjectGuid
        columnName: incidentId
alertDetailsOverride:
  alertDisplayNameFormat: '{{incidentName}}'
  alertDescriptionFormat: '{{summary}}'
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: Selected
    groupByEntities:
      - SecurityGroup
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

ContrastADRIncidents_CL

Darktrace Model Breach (Legacy)

#

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.

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

'This rule creates Microsoft Sentinel Alerts based on Darktrace Model Breaches, fetched every 5 minutes.'

MITRE ATT&CK Coverage

Rule Body

id: a3c7b8ed-56a9-47b7-98e5-2555c16e17c9
name: Darktrace Model Breach (Legacy)
description: |
  'This rule creates Microsoft Sentinel Alerts based on Darktrace Model Breaches, fetched every 5 minutes.'
severity: Medium
requiredDataConnectors:
  - connectorId: DarktraceRESTConnector
    dataTypes:
      - darktrace_model_alerts_CL
queryFrequency: 5m
queryPeriod: 5m 
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - LateralMovement
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1059
  - T1021
  - T1071
query: |
  darktrace_model_alerts_CL 
  | where dtProduct_s =="Policy Breach"
  | project-rename EventSeverity=score_d, EventStartTime=breachTime_s, NetworkRuleName=modelName_s, NetworkRuleNumber=pid_d, ThreatIdentifier=threatID_d, ThreatCategory=dtProduct_s, SrcIpAddr=SourceIP, SrcHostname=hostname_s, SrcMacAddr=sourceMac_s, SrcPortNumber=sourcePort_s, DstIpAddr=destIP_s, DstPortNumber=destPort_s, DstHostname=destHost_s, DstMacAddr=destMac_s, DtCompliance=compliance_b, DtClientSensor=cSensor_b, DtDescription=description_s, DtAntigena=antigena_b, DtDeviceIdentifier=deviceId_d, DtSubnetID=sid_d, DtTags=tags_s, DtMitreTechniques=mitreTechniques_s, DtMessage=Message, DtUUID=uuid_g, DtBreachURL=breachUrl_s, DtSrcTypeLabel=typeLabel_s, DtTriggeredComponents=triggeredComponents_s, DtDetails=details_s, DtLongitude=longitude_d, DtLatitude=latitude_d, DtCategory=Category
  | extend EventCount=1, EventType="NetworkSession", EventSchema="NetworkSession", EventSchemaVersion="0.2.2", EventResult="Success", DvcAction = "Allow", EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", EventEndTime=EventStartTime, ThreatName=NetworkRuleName, ThreatRiskLevel=EventSeverity
  | extend DtSentinelCategory = case(DtCategory == "Suspicious", "Medium", 
                                    DtCategory == "Critical", "High",
                                    "Informational") 
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DstHostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DstIpAddr
customDetails:
  SrcMacAddr: SrcMacAddr
  EventSeverity: EventSeverity
  EventStartTime: EventStartTime
  NetworkRuleName: NetworkRuleName
  NetworkRuleNumber: NetworkRuleNumber
  ThreatIdentifier: ThreatIdentifier
  DtSentinelCategory: DtSentinelCategory
  SrcPortNumber: SrcPortNumber
  DstPortNumber: DstPortNumber
  DstMacAddr: DstMacAddr
  DtCompliance: DtCompliance
  DtDescription: DtDescription
  DtCategory: DtCategory
  DtDeviceIdentifier: DtDeviceIdentifier
# These are described here - this is why we're leaving tactics and techniques above empty
alertDetailsOverride:
  # model breach name here
  alertDisplayNameFormat: 'Darktrace: {{ThreatRiskLevel}} - {{NetworkRuleName}}' # Up to 256 chars and 3 placeholders
  alertDescriptionFormat: '{{DtMessage}}' # Up to 5000 chars and 3 placeholders
  # MITRE tactic
  alertTacticsColumnName: # leave empty
  alertSeverityColumnName: # leave empty
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: DtBreachURL
    - alertProperty: ProviderName
      value: EventVendor
    - alertProperty: ProductName
      value: EventProduct
    - alertProperty: ProductComponentName
      value: ThreatCategory
    - alertProperty: Severity
      value: DtSentinelCategory
version: 1.1.1
kind: NRT

Stages and Predicates

Stage 1: source

darktrace_model_alerts_CL

Stage 2: where

| where dtProduct_s =="Policy Breach"

Stage 3: project-rename

| project-rename EventSeverity=score_d, EventStartTime=breachTime_s, NetworkRuleName=modelName_s, NetworkRuleNumber=pid_d, ThreatIdentifier=threatID_d, ThreatCategory=dtProduct_s, SrcIpAddr=SourceIP, SrcHostname=hostname_s, SrcMacAddr=sourceMac_s, SrcPortNumber=sourcePort_s, DstIpAddr=destIP_s, DstPortNumber=destPort_s, DstHostname=destHost_s, DstMacAddr=destMac_s, DtCompliance=compliance_b, DtClientSensor=cSensor_b, DtDescription=description_s, DtAntigena=antigena_b, DtDeviceIdentifier=deviceId_d, DtSubnetID=sid_d, DtTags=tags_s, DtMitreTechniques=mitreTechniques_s, DtMessage=Message, DtUUID=uuid_g, DtBreachURL=breachUrl_s, DtSrcTypeLabel=typeLabel_s, DtTriggeredComponents=triggeredComponents_s, DtDetails=details_s, DtLongitude=longitude_d, DtLatitude=latitude_d, DtCategory=Category

Stage 4: extend

| extend EventCount=1, EventType="NetworkSession", EventSchema="NetworkSession", EventSchemaVersion="0.2.2", EventResult="Success", DvcAction = "Allow", EventVendor = "Darktrace", EventProduct = "Darktrace DETECT", EventEndTime=EventStartTime, ThreatName=NetworkRuleName, ThreatRiskLevel=EventSeverity

Stage 5: extend

| extend DtSentinelCategory = case(DtCategory == "Suspicious", "Medium", 
                                  DtCategory == "Critical", "High",
                                  "Informational")
DtSentinelCategory =
ifDtCategory == "Suspicious""Medium"
elifDtCategory == "Critical""High"
else"Informational"

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstHostnameproject-rename
DstIpAddrproject-rename
DstMacAddrproject-rename
DstPortNumberproject-rename
DtAntigenaproject-rename
DtBreachURLproject-rename
DtCategoryproject-rename
DtClientSensorproject-rename
DtComplianceproject-rename
DtDescriptionproject-rename
DtDetailsproject-rename
DtDeviceIdentifierproject-rename
DtLatitudeproject-rename
DtLongitudeproject-rename
DtMessageproject-rename
DtMitreTechniquesproject-rename
DtSrcTypeLabelproject-rename
DtSubnetIDproject-rename
DtTagsproject-rename
DtTriggeredComponentsproject-rename
DtUUIDproject-rename
EventSeverityproject-rename
EventStartTimeproject-rename
NetworkRuleNameproject-rename
NetworkRuleNumberproject-rename
SrcHostnameproject-rename
SrcIpAddrproject-rename
SrcMacAddrproject-rename
SrcPortNumberproject-rename
ThreatCategoryproject-rename
ThreatIdentifierproject-rename
DvcActionextend
EventCountextend
EventEndTimeextend
EventProductextend
EventResultextend
EventSchemaextend
EventSchemaVersionextend
EventTypeextend
EventVendorextend
ThreatNameextend
ThreatRiskLevelextend
DtSentinelCategoryextend

Detect Multiple Hello for Business PRT tokens being used simultaneously for one device.

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

This detection rule tries to find multiple PRT tokens being used simultaneously for one device. This might indicate that an attacker was able to request a new PRT on a second device using exxported Windows Hello for Business keys. More information about the attack scenario can be found in the references.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

References

Telemetry Coverage

Rule Body

// Get the Sign-in logs we want to query
let base = materialize(
    SigninLogs
    | where Timestamp > ago(1d)
);
// Get all the WHfB signins by looking at the authentication method and incomming token
let whfb = (
    base
    // Get WHfB signins
    | mv-expand todynamic(AuthenticationDetails)
    | where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
    | where IncomingTokenType == "primaryRefreshToken"
    | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
    // Remove empty Session and Device IDs
    | where SessionId != "" and DeviceID != ""
);
// Save the time frame for each WHfB PRT token
// We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used
let prt_timeframes = (
    whfb
    // Summarize the first and last PRT usage per device, by using the Session ID
    | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
    | project DeviceID, SessionId, TimeMin, TimeMax
);
// Save all the Session IDs for the logins that came from a WHfB authentication method
let whfb_sessions = toscalar(
    whfb
    | summarize make_set(SessionId)
);
base
| mv-expand todynamic(AuthenticationDetails)
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Get all signins related to a WHfB Session
| where SessionId in (whfb_sessions)
// Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device
| join kind=inner prt_timeframes on DeviceID
| extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName)
// Get logins where the current SessionID is not the same as another one
| where CurrentSessionID != OtherSessionID
// Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID)
| summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID
| where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax)
// Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore)
| where AppDisplayName != "Windows Sign In"
| project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName

// Get the Sign-in logs we want to query
let base = materialize(
    SigninLogs
    | where TimeGenerated > ago(1d)
);
// Get all the WHfB signins by looking at the authentication method and incomming token
let whfb = (
    base
    // Get WHfB signins
    | mv-expand todynamic(AuthenticationDetails)
    | where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
    | where IncomingTokenType == "primaryRefreshToken"
    | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
    // Remove empty Session and Device IDs
    | where SessionId != "" and DeviceID != ""
);
// Save the time frame for each WHfB PRT token
// We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used
let prt_timeframes = (
    whfb
    // Summarize the first and last PRT usage per device, by using the Session ID
    | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
    | project DeviceID, SessionId, TimeMin, TimeMax
);
// Save all the Session IDs for the logins that came from a WHfB authentication method
let whfb_sessions = toscalar(
    whfb
    | summarize make_set(SessionId)
);
base
| mv-expand todynamic(AuthenticationDetails)
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Get all signins related to a WHfB Session
| where SessionId in (whfb_sessions)
// Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device
| join kind=inner prt_timeframes on DeviceID
| extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName)
// Get logins where the current SessionID is not the same as another one
| where CurrentSessionID != OtherSessionID
// Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID)
| summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID
| where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax)
// Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore)
| where AppDisplayName != "Windows Sign In"
| project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName

Stages and Predicates

let base is inlined into the numbered stages below.

Let Binding: whfb

let whfb = (
    base
    | mv-expand todynamic(AuthenticationDetails)
    | where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
    | where IncomingTokenType == "primaryRefreshToken"
    | extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
    | where SessionId != "" and DeviceID != ""
);

Let Binding: prt_timeframes used in Stage 6

let prt_timeframes = (
    whfb
    | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
    | project DeviceID, SessionId, TimeMin, TimeMax
);

Let Binding: whfb_sessions used in Stage 5

let whfb_sessions = toscalar(
    whfb
    | summarize make_set(SessionId)
);

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

Stage 1: source

SigninLogs

Stage 2: where

| where Timestamp > ago(1d)

Stage 3: mv-expand

base
| mv-expand todynamic(AuthenticationDetails)

Stage 4: extend

| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)

Stage 5: where

| where SessionId in (whfb_sessions)

Stage 6: join

| join kind=inner prt_timeframes on DeviceID

Stage 7: extend

extend CurrentSessionID, DeviceName, OtherSessionID, OtherSessionTimeMax, OtherSessionTimeMin

Stage 8: where

where CurrentSessionID != OtherSessionID

Stage 9: summarize

summarize by DeviceID, CurrentSessionID

Stage 10: where

where AuthenticationDateTime >= "OtherSessionTimeMin" and AuthenticationDateTime <= "OtherSessionTimeMax"

Stage 11: where

where AppDisplayName != "Windows Sign In"

Stage 12: project

project AppDisplayName, AuthenticationDateTime, CurrentSessionID, DeviceID, DeviceName, OtherSessionID, OtherSessionTimeMax, OtherSessionTimeMin, ResourceDisplayName, UserPrincipalName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
AuthenticationDateTimeproject
CurrentSessionIDproject
DeviceIDproject
DeviceNameproject
OtherSessionIDproject
OtherSessionTimeMaxproject
OtherSessionTimeMinproject
ResourceDisplayNameproject
UserPrincipalNameproject

DomainTools Newly Added Domains Feeds Ingested

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

Creates an incident whenever DomainTools Newly Added Domains new Feeds (domains) are ingested in the last 1 hour.

Rule Body

id: f8c223c1-1a28-4b28-8e9f-5e34b610916b
name: DomainTools Newly Added Domains Feeds Ingested
description: Creates an incident whenever DomainTools Newly Added Domains new Feeds (domains) are ingested in the last 1 hour.
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: DomainTools
    dataTypes:
      - DomainToolsThreatIntelDomains_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "DomainToolsThreatIntelDomains(1h,\"nad\")"
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainCustomEntity
eventGroupingSettings:
  aggregationKind: SingleAlert
version: 1.0.0

Stages and Predicates

Stage 1: source

DomainToolsThreatIntelDomains(1h,"nad")

Failed logon attempts in authpriv

#
Severity
medium
Time window
1d
Group by
Computer, HostIP, ProcessID, RemoteIP
Source
github.com/Azure/Azure-Sentinel

'Identifies failed logon attempts from unknown users in Syslog authpriv logs. The unknown user means the account that tried to log in isn't provisioned on the machine. A few hits could indicate someone attempting to access a machine they aren't authorized to access. If there are many of hits, especially from outside your network, it could indicate a brute force attack. Default threshold for logon attempts is 15.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: e7ec9fa6-e7f7-41ed-a34b-b956837a3ee6
name: Failed logon attempts in authpriv
description: |
  'Identifies failed logon attempts from unknown users in Syslog authpriv logs. The unknown user means the account that tried to log in isn't provisioned on the machine. A few hits could indicate someone attempting to access a machine they aren't authorized to access. 
  If there are many of hits, especially from outside your network, it could indicate a brute force attack. 
  Default threshold for logon attempts is 15.'
severity: Medium
requiredDataConnectors:
  - connectorId: Syslog
    dataTypes: 
      - Syslog
  - connectorId: SyslogAma
    dataTypes: 
      - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let threshold = 15;
  // Below pulls messages from syslog-authpriv logs where there was an authentication failure with an unknown user.
  // IP address of system attempting logon is also extracted from the SyslogMessage field. Some of these messages
  // are aggregated.
  Syslog
  | where Facility =~ "authpriv"
  | where SyslogMessage has "authentication failure" and SyslogMessage has " uid=0"
  | extend RemoteIP = extract(@".*?rhost=([\d.]+).*?", 1,SyslogMessage)
  | project TimeGenerated, Computer, ProcessName, HostIP, RemoteIP, ProcessID
  | join kind=innerunique (
      // Below pulls messages from syslog-authpriv logs that show each instance an unknown user tried to logon. 
      Syslog 
      | where Facility =~ "authpriv"
      | where SyslogMessage has "user unknown"
      | project Computer, HostIP, ProcessID
      ) on Computer, HostIP, ProcessID
  // Count the number of failed logon attempts by External IP and internal machine
  | summarize FirstLogonAttempt = min(TimeGenerated), LatestLogonAttempt = max(TimeGenerated), TotalLogonAttempts = count() by Computer, HostIP, RemoteIP
  // Calculate the time between first and last logon attempt (AttemptPeriodLength)
  | extend TimeBetweenLogonAttempts = LatestLogonAttempt - FirstLogonAttempt
  | where TotalLogonAttempts >= threshold
  | project FirstLogonAttempt, LatestLogonAttempt, TimeBetweenLogonAttempts, TotalLogonAttempts, SourceAddress = RemoteIP, Computer,  HostIP
  | sort by Computer asc nulls last
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: HostIP
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 15;

Stage 1: source

Syslog

Stage 2: where

| where Facility =~ "authpriv"

Stage 3: where

| where SyslogMessage has "authentication failure" and SyslogMessage has " uid=0"

Stage 4: extend

| extend RemoteIP = extract(@".*?rhost=([\d.]+).*?", 1,SyslogMessage)

Stage 5: project

| project TimeGenerated, Computer, ProcessName, HostIP, RemoteIP, ProcessID

Stage 6: join

| join kind=innerunique (
    Syslog 
    | where Facility =~ "authpriv"
    | where SyslogMessage has "user unknown"
    | project Computer, HostIP, ProcessID
    ) on Computer, HostIP, ProcessID

Stage 7: summarize

summarize FirstLogonAttempt, LatestLogonAttempt, TotalLogonAttempts by Computer, HostIP, RemoteIP

Stage 8: extend

extend TimeBetweenLogonAttempts

Stage 9: where

where TotalLogonAttempts >= 15

Stage 10: project

project Computer, FirstLogonAttempt, HostIP, LatestLogonAttempt, SourceAddress, TimeBetweenLogonAttempts, TotalLogonAttempts

Stage 11: sort

sort by Computer

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Facilityeq
  • authpriv
field:"Facility" kind:eq value:"authpriv"
SyslogMessagematch
  • uid=0 transforms: term
  • authentication failure transforms: term
  • user unknown transforms: term
field:"SyslogMessage" kind:match
TotalLogonAttemptsge
  • 15 transforms: number
field:"TotalLogonAttempts" kind:ge value:"15"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
FirstLogonAttemptproject
HostIPproject
LatestLogonAttemptproject
SourceAddressproject
TimeBetweenLogonAttemptsproject
TotalLogonAttemptsproject

Guardian- Ban Topic Policy Violation Detection

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

'This alert creates an incident when Ban Topic Policy Violation detected from the Guardian.'

Rule Body

id: 97a76b2d-02ea-4ae5-b0c4-79e834036481
name: Guardian- Ban Topic Policy Violation Detection
description: |
  'This alert creates an incident when Ban Topic Policy Violation detected from the Guardian.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Ban Topic'
  | where Severity =~ 'Medium'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Ban Topic Policy Violation detection
  alertDescriptionFormat: |
    This query detects Ban Topic Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Ban Topic'

Stage 3: where

| where Severity =~ 'Medium'

Indicators

These rows show field, operator, and value matches.

Hunt for critical credentials on devices with non-critical accounts

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

In most organizations normal user accounts or accounts with low risk permissions have less security controls enabled. This because there are less security controls needed in order to minize the risk vectors that come with these accounts. If these accounts are used on devices where critical account credentials are also present, the critical user account can be compromised more easily when the device is accessed by an adversary via the non-critical user account. Because of this, a Privileged Access Workstation should be used which serves as a dedicated workstation for the critical accounts. By doing this, the critical user account cannot be compromised via a unhardened device.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry Coverage

Rule Body

// Search for all users and save their criticality level
let xspm_users = materialize(
    ExposureGraphNodes
    | where NodeLabel == "user"
    | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
    | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
    | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
);
// Make a list of all critical users
let critical_users = toscalar(
    xspm_users
    | where CriticalityLevel == 0
    | summarize make_set(NodeName)
);
// Make a list of all non critical users
let non_critical_users = toscalar(
    xspm_users
    | where CriticalityLevel != 0
    | summarize make_set(NodeName)
);
// Make graph for max of 3 edges, where we start from a device and end with an user
ExposureGraphEdges
| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
    where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user"
    project SourceNodeName = SourceNode.NodeName,
    Edges = anyEdge.EdgeLabel,
    TargetNodeName = TargetNode.NodeName,
    TargetNodeLabel = TargetNode.NodeLabel
// Make a list of all users a device has credentials for
| summarize UserList = make_set(TargetNodeName) by SourceNodeName
// Only return devices with more than one credential
| where array_length(UserList) > 1
// Make new lists saving the critical users and non critical users per device
| extend CriticalUserList = set_intersect(UserList, critical_users),
    NonCriticalUserList = set_intersect(UserList, non_critical_users)
// Flag when a device has both critical and non critical users
| where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0

Stages and Predicates

Let Binding: xspm_users

let xspm_users = materialize(
    ExposureGraphNodes
    | where NodeLabel == "user"
    | extend CriticalityLevel = todynamic(NodeProperties).rawData.criticalityLevel.criticalityLevel
    | extend RuleNames = todynamic(NodeProperties).rawData.criticalityLevel.ruleNames
    | distinct NodeName, NodeId, tostring(CriticalityLevel), tostring(RuleNames)
);

Let Binding: critical_users used in Stage 6

let critical_users = toscalar(
    xspm_users
    | where CriticalityLevel == 0
    | summarize make_set(NodeName)
);

Let Binding: non_critical_users used in Stage 6

let non_critical_users = toscalar(
    xspm_users
    | where CriticalityLevel != 0
    | summarize make_set(NodeName)
);

Stage 1: source

ExposureGraphEdges

Stage 2: macro

| make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId

Stage 3: macro

| graph-match (SourceNode)-[anyEdge*1..3]->(TargetNode)
    where SourceNode.NodeLabel in ("device", "microsoft.compute/virtualmachines") and TargetNode.NodeLabel == "user"
    project SourceNodeName = SourceNode.NodeName,
    Edges = anyEdge.EdgeLabel,
    TargetNodeName = TargetNode.NodeName,
    TargetNodeLabel = TargetNode.NodeLabel

Stage 4: summarize

| summarize UserList = make_set(TargetNodeName) by SourceNodeName

Stage 5: where

| where array_length(UserList) > 1

Stage 6: extend

| extend CriticalUserList = set_intersect(UserList, critical_users),
    NonCriticalUserList = set_intersect(UserList, non_critical_users)

Stage 7: where

| where array_length(CriticalUserList) > 0 and array_length(NonCriticalUserList) > 0

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceNodeNamesummarize
UserListsummarize
CriticalUserListextend
NonCriticalUserListextend

Identify SysAid Server web shell creation

#
Severity
high
Time window
5m
Group by
Account, CommandLine, Computer, ObjectName, ParentProcessName, Process, ProcessName, SubjectLogonId, TimeGenerated, timekey
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query looks for potential webshell creation by the threat actor Mercury after the sucessful exploitation of SysAid server. Reference: https://www.microsoft.com/security/blog/2022/08/25/mercury-leveraging-log4j-2-vulnerabilities-in-unpatched-systems-to-target-israeli-organizations/

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 50eb4cbd-188f-44f4-b964-bab84dcdec10
name: Identify SysAid Server web shell creation 
description: |
  'This query looks for potential webshell creation by the threat actor Mercury after the sucessful exploitation of SysAid server. 
  Reference:  https://www.microsoft.com/security/blog/2022/08/25/mercury-leveraging-log4j-2-vulnerabilities-in-unpatched-systems-to-target-israeli-organizations/'
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent 
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent 
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceFileEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
tags:
  - Mercury
  - Schema: ASIMFileEvent
    SchemaVersion: 0.1.0
query: |
  let timeframe = 1d;
  let time_window = 5m;
  (union isfuzzy=true
  (SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4688
  | where Process has_any ("java.exe", "javaw.exe") and CommandLine has "SysAidServer" 
  | summarize by ParentProcessName,Process, Account, Computer, CommandLine, timekey= bin(TimeGenerated, time_window), TimeGenerated, SubjectLogonId
  | join kind=inner(
  SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4663
  | where Process has_any ("java.exe", "javaw.exe")
  | where AccessMask in ('0x2','0x100', '0x10', '0x4')
  | where ObjectName endswith ".jsp" 
  | summarize by ParentProcessName, Account, Computer, ObjectName, ProcessName, timekey= bin(TimeGenerated, time_window), TimeGenerated, SubjectLogonId)
   on timekey, Computer, SubjectLogonId
  ),
  (DeviceFileEvents 
  | where InitiatingProcessFileName has_any ("java.exe", "javaw.exe")  
  | where InitiatingProcessCommandLine has "SysAidServer"  
  | where FileName endswith ".jsp" 
  | extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
  ),
  (imFileEvent
  | where TimeGenerated > ago(timeframe)
  | where EventType == "FileCreated"
  | where ActingProcessName has_any ("java.exe", "javaw.exe") 
  | where ActingProcessCommandLine has "SysAidServer"  
  | where FilePath endswith ".jsp" 
  | extend Account = ActorUsername, Computer = DvcHostname
  )
  )
  | extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - 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.3
kind: Scheduled
metadata:
    source:
        kind: Scheduled
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let timeframe = 1d;
let time_window = 5m;

Union isfuzzy=true (3 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, DeviceFileEvents, imFileEvent

Leg 1: SecurityEvent

SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4688
| where Process has_any ("java.exe", "javaw.exe") and CommandLine has "SysAidServer" 
| summarize by ParentProcessName,Process, Account, Computer, CommandLine, timekey= bin(TimeGenerated, time_window), TimeGenerated, SubjectLogonId
| join kind=inner(
SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4663
| where Process has_any ("java.exe", "javaw.exe")
| where AccessMask in ('0x2','0x100', '0x10', '0x4')
| where ObjectName endswith ".jsp" 
| summarize by ParentProcessName, Account, Computer, ObjectName, ProcessName, timekey= bin(TimeGenerated, time_window), TimeGenerated, SubjectLogonId)
 on timekey, Computer, SubjectLogonId

Leg 2: DeviceFileEvents

DeviceFileEvents 
| where InitiatingProcessFileName has_any ("java.exe", "javaw.exe")  
| where InitiatingProcessCommandLine has "SysAidServer"  
| where FileName endswith ".jsp" 
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName

Leg 3: imFileEvent

imFileEvent
| where TimeGenerated > ago(timeframe)
| where EventType == "FileCreated"
| where ActingProcessName has_any ("java.exe", "javaw.exe") 
| where ActingProcessCommandLine has "SysAidServer"  
| where FilePath endswith ".jsp" 
| extend Account = ActorUsername, Computer = DvcHostname

Applied to the Combined Result

| extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountsummarize
Computersummarize
ObjectNamesummarize
ParentProcessNamesummarize
ProcessNamesummarize
SubjectLogonIdsummarize
TimeGeneratedsummarize
timekeysummarize

Infoblox - IQ for TD Detected Insights - API Source

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

'Infoblox IQ for TD Insight detected in logs sourced via REST API. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxInsight.'

MITRE ATT&CK Coverage

Rule Body

id: a5e2df87-f0c9-4540-8715-96e71b608986
name: Infoblox - IQ for TD Detected Insights - API Source
description: |
  'Infoblox IQ for TD Insight detected in logs sourced via REST API. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxInsight**](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Infoblox/Parsers/InfobloxInsight.yaml).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: InfobloxSOCInsightsDataConnector_API
    dataTypes: 
      - InfobloxInsight
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1565
query: |
  InfobloxInsight
  | summarize arg_max(TimeGenerated, *) by InfobloxInsightID
  | where DataObservedVia != "CDC" and InsightDetail != true
  | extend ThreatProperties = strcat_array(ThreatProperties, ", ")
entityMappings:
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: Category
        columnName: ThreatProperties
customDetails:
  InfobloxInsightID: InfobloxInsightID
  Severity: Priority
  Status: Status
  FirstSeen: FirstSeen
  EvaluationStartDate: EvaluationStartDate
  EvaluationEndDate: EvaluationEndDate
  TotalHits: EventsCount
  TotalAssets: TotalAssets
  TotalIndicators: TotalIndicators
  ThreatActor: ThreatActor
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Infoblox - IQ for TD Insight - {{Name}}'
  alertDescriptionFormat: 'Observed via API. {{Description}} Observation: {{FirstSeen}}'
  alertSeverityColumnName: IncidentSeverity
incidentConfiguration:
  createIncident: true
version: 1.1.0
kind: Scheduled

Stages and Predicates

Stage 1: source

InfobloxInsight

Stage 2: summarize

| summarize arg_max(TimeGenerated, *) by InfobloxInsightID

Stage 3: where

| where DataObservedVia != "CDC" and InsightDetail != true

Stage 4: extend

| extend ThreatProperties = strcat_array(ThreatProperties, ", ")

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
InfobloxInsightIDsummarize
ThreatPropertiesextend

Jira - User removed from project

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

'Detects when a user was removed from project.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 5d3af0aa-833e-48ed-a29a-8cfd2705c953
name: Jira - User removed from project
description: |
  'Detects when a user was removed from project.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: JiraAuditAPI
    dataTypes:
      - JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1531
query: |
  JiraAudit
  | where EventMessage =~ 'User removed from project'
  | extend user = todynamic(AssociatedItems)[0]['name']
  | project EventCreationTime, ObjectItemName, user
  | extend AccountCustomEntity = user
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

JiraAudit

Stage 2: where

| where EventMessage =~ 'User removed from project'

Stage 3: extend

| extend user = todynamic(AssociatedItems)[0]['name']

Stage 4: project

| project EventCreationTime, ObjectItemName, user

Stage 5: extend

| extend AccountCustomEntity = user

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventCreationTimeproject
ObjectItemNameproject
userproject
AccountCustomEntityextend

M2131_EventLogManagementPostureChanged_EL0

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

'This alert is desinged to monitor Azure policies aligned with the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when EL0 policy compliance falls below 70% within a 1 week timeframe.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 1f8fcca5-47ed-409d-a8fa-d49ef821feaf
name: M2131_EventLogManagementPostureChanged_EL0
description: |
  'This alert is desinged to monitor Azure policies aligned with the Maturity Model for Event Log Management (M-21-31) standard. The alert triggers when EL0 policy compliance falls below 70% within a 1 week timeframe.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 7d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  SecurityRecommendation
  | where RecommendationDisplayName <> ""
  | extend MaturityLevel=iff(RecommendationDisplayName has_any("agent","extension","retention","logs encryption","collect","retained","log profile","CloudTrail","metric","AWS","GCP","DNS","Auditing","Flow","logging","usage"), "Event Logging (EL0)",
  iff(RecommendationDisplayName has_any("container registries","logic apps","Enhanced monitoring","IoT Hub","Event Hub","App Service", "Kubernetes","updates","email", "automation", "adaptive"), "Advanced Event Logging (EL3)",
  iff(RecommendationDisplayName has_any("signatures","CMK","CMEK","double encryption","managed key","KMS","container","Watcher"), "Intermediate Event Logging (EL2)",
  iff(RecommendationDisplayName has_any("Exploit Guard","endpoint protection","Antimalware","health","VPC","Defender","Vulnerabilities","vulnerability","diagnostic","Key","activity log alert",""), "Basic Event Logging (EL1)","Other"))))
  | summarize arg_max(TimeGenerated, *) by RecommendationDisplayName, AssessedResourceId, MaturityLevel
  | summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by MaturityLevel
  | extend PassedControls = (Passed/todouble(Total))*100
  | extend RemediationLink = strcat('https://portal.azure.com/#blade/Microsoft_Azure_Security/SecurityMenuBlade/22')
  | project MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()
  | where MaturityLevel <> ''
  | where MaturityLevel == "Event Logging (EL0)"
  | where PassedControls < 70 
  //Adjust Either FailedRatePercentage or PasedRatePercentage Thresholds within   Organizational Needs
  | sort by PassedControls desc
  | extend URLCustomEntity = RemediationLink
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URLCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityRecommendation

Stage 2: where

| where RecommendationDisplayName <> ""

Stage 3: extend

| extend MaturityLevel=iff(RecommendationDisplayName has_any("agent","extension","retention","logs encryption","collect","retained","log profile","CloudTrail","metric","AWS","GCP","DNS","Auditing","Flow","logging","usage"), "Event Logging (EL0)",
iff(RecommendationDisplayName has_any("container registries","logic apps","Enhanced monitoring","IoT Hub","Event Hub","App Service", "Kubernetes","updates","email", "automation", "adaptive"), "Advanced Event Logging (EL3)",
iff(RecommendationDisplayName has_any("signatures","CMK","CMEK","double encryption","managed key","KMS","container","Watcher"), "Intermediate Event Logging (EL2)",
iff(RecommendationDisplayName has_any("Exploit Guard","endpoint protection","Antimalware","health","VPC","Defender","Vulnerabilities","vulnerability","diagnostic","Key","activity log alert",""), "Basic Event Logging (EL1)","Other"))))
MaturityLevel =
if(RecommendationDisplayName has "agent" or RecommendationDisplayName has "extension" or RecommendationDisplayName has "retention" or RecommendationDisplayName has "logs encryption" or RecommendationDisplayName has "collect" or RecommendationDisplayName has "retained" or RecommendationDisplayName has "log profile" or RecommendationDisplayName has "CloudTrail" or RecommendationDisplayName has "metric" or RecommendationDisplayName has "AWS" or RecommendationDisplayName has "GCP" or RecommendationDisplayName has "DNS" or RecommendationDisplayName has "Auditing" or RecommendationDisplayName has "Flow" or RecommendationDisplayName has "logging" or RecommendationDisplayName has "usage")"Event Logging (EL0)"
elseiff((RecommendationDisplayName has_any ("container registries", "logic apps", "Enhanced monitoring", "IoT Hub", "Event Hub", "App Service", "Kubernetes", "updates", "email", "automation", "adaptive")), "Advanced Event Logging (EL3)", iff((RecommendationDisplayName has_any ("signatures", "CMK", "CMEK", "double encryption", "managed key", "KMS", "container", "Watcher")), "Intermediate Event Logging (EL2)", iff((RecommendationDisplayName has_any ("Exploit Guard", "endpoint protection", "Antimalware", "health", "VPC", "Defender", "Vulnerabilities", "vulnerability", "diagnostic", "Key", "activity log alert", "")), "Basic Event Logging (EL1)", "Other")))

Stage 4: summarize

| summarize arg_max(TimeGenerated, *) by RecommendationDisplayName, AssessedResourceId, MaturityLevel

Stage 5: summarize

| summarize Failed = countif(RecommendationState == "Unhealthy"), Passed = countif(RecommendationState == "Healthy"), Total = countif(RecommendationState == "Healthy" or RecommendationState == "Unhealthy") by MaturityLevel

Stage 6: extend

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

Stage 7: extend

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

Stage 8: project

| project MaturityLevel, Total, PassedControls, Passed, Failed, RemediationLink, LastObserved=now()

Stage 9: where

| where MaturityLevel <> ''

Stage 10: where

| where MaturityLevel == "Event Logging (EL0)"

Stage 11: where

| where PassedControls < 70

Stage 12: sort

| sort by PassedControls desc

Stage 13: 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
MaturityLevelproject
Passedproject
PassedControlsproject
RemediationLinkproject
Totalproject
URLCustomEntityextend

Mass Download & copy to USB device by single user

#
Severity
medium
Time window
1d
Group by
$left.UserId, $right.InitiatingProcessAccountUpn, AADUserId, Alert_TimeGenerated, CloudAppEvents_Details, DeviceFileEvents_Details, DeviceId, DeviceName, IPAddress, USB_Details, USB_TimeGenerated, UserId
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query looks for any mass download by a single user with possible file copy activity to a new USB drive. Malicious insiders may perform such activities that may cause harm to the organization. This query could also reveal unintentional insider that had no intention of malicious activity but their actions may impact an organizations security posture. Reference:https://docs.microsoft.com/defender-cloud-apps/policy-template-reference

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rule Body

id: 6267ce44-1e9d-471b-9f1e-ae76a6b7aa84
name: Mass Download & copy to USB device by single user
description: |
  'This query looks for any mass download by a single user with possible file copy activity to a new USB drive. Malicious insiders may perform such activities that may cause harm to the organization. 
  This query could also reveal unintentional insider that had no intention of malicious activity but their actions may impact an organizations security posture.
  Reference:https://docs.microsoft.com/defender-cloud-apps/policy-template-reference'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - CloudAppEvents
      - DeviceEvents
      - DeviceFileEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1052
query: |
  let Alerts = SecurityAlert
  | where AlertName =~ "mass download by a single user"
  | where Status != 'Resolved'
  | extend ipEnt = parse_json(Entities), accountEnt = parse_json(Entities)
  | mv-apply tempParams = ipEnt on (
  mv-expand ipEnt
  | where ipEnt.Type == "ip" 
  | extend IpAddress = tostring(ipEnt.Address)
  )
  | mv-apply tempParams = accountEnt on (
  mv-expand accountEnt
  | where accountEnt.Type == "account"
  | extend AADUserId = tostring(accountEnt.AadUserId)
  )
  | extend Alert_TimeGenerated = TimeGenerated
  | distinct Alert_TimeGenerated, IpAddress, AADUserId, DisplayName, Description, ProductName, ExtendedProperties, Entities, Status, CompromisedEntity
  ;
  let CA_Events = CloudAppEvents
  | where ActionType == "FileDownloaded"
  | extend parsed = parse_json(RawEventData)
  | extend UserId = tostring(parsed.UserId)
  | extend FileName = tostring(parsed.SourceFileName)
  | extend FileExtension = tostring(parsed.SourceFileExtension)
  | summarize CloudAppEvent_StartTime = min(TimeGenerated), CloudAppEvent_EndTime = max(TimeGenerated), CloudAppEvent_Files = make_set(FileName), FileCount = dcount(FileName) by Application, AccountObjectId, UserId, IPAddress, City, CountryCode
  | extend CloudAppEvents_Details = pack_all();
  let CA_Alerts_Events = Alerts | join kind=inner (CA_Events)
  on $left.AADUserId == $right.AccountObjectId and $left.IpAddress == $right.IPAddress
  // Cloud app event comes before Alert
  | where CloudAppEvent_EndTime <= Alert_TimeGenerated
  | project Alert_TimeGenerated, UserId, AADUserId, IPAddress, CloudAppEvents_Details, CloudAppEvent_Files
  ;
  // setup list to filter DeviceFileEvents for only files downloaded as indicated by CloudAppEvents
  let CA_FileList = CA_Alerts_Events | project CloudAppEvent_Files;
  CA_Alerts_Events
  | join kind=inner ( DeviceFileEvents
  | where ActionType in ("FileCreated", "FileRenamed")
  | where FileName in~ (CA_FileList)
  | summarize DeviceFileEvent_StartTime = min(TimeGenerated), DeviceFileEvent_EndTime = max(TimeGenerated), DeviceFileEvent_Files = make_set(FolderPath), DeviceFileEvent_FileCount = dcount(FolderPath) by InitiatingProcessAccountUpn, DeviceId, DeviceName, InitiatingProcessFolderPath, InitiatingProcessParentFileName//, InitiatingProcessCommandLine
  | extend DeviceFileEvents_Details = pack_all()
  ) on $left.UserId == $right.InitiatingProcessAccountUpn
  | where DeviceFileEvent_StartTime >= Alert_TimeGenerated
  | join kind=inner (
  // get device events where a USB drive was mounted
  DeviceEvents
  | where ActionType == "UsbDriveMounted"
  | extend parsed = parse_json(AdditionalFields)
  | extend USB_DriveLetter = tostring(AdditionalFields.DriveLetter), USB_ProductName = tostring(AdditionalFields.ProductName), USB_Volume = tostring(AdditionalFields.Volume)
  | where isnotempty(USB_DriveLetter)
  | project USB_TimeGenerated = TimeGenerated, DeviceId, USB_DriveLetter, USB_ProductName, USB_Volume
  | extend USB_Details = pack_all()
  )  
  on DeviceId
  // USB event occurs after the Alert
  | where USB_TimeGenerated >= Alert_TimeGenerated
  | mv-expand DeviceFileEvent_Files
  | extend DeviceFileEvent_Files = tostring(DeviceFileEvent_Files)
  // make sure that we only pickup the files that have the USB drive letter
  | where DeviceFileEvent_Files startswith USB_DriveLetter
  | summarize USB_Drive_MatchedFiles = make_set_if(DeviceFileEvent_Files, DeviceFileEvent_Files startswith USB_DriveLetter) by Alert_TimeGenerated, USB_TimeGenerated, UserId, AADUserId, DeviceId, DeviceName, IPAddress, CloudAppEvents_Details = tostring(CloudAppEvents_Details), DeviceFileEvents_Details = tostring(DeviceFileEvents_Details), USB_Details = tostring(USB_Details)
  | extend InitiatingProcessFileName = tostring(split(todynamic(DeviceFileEvents_Details).InitiatingProcessFolderPath, "\\")[-1]), InitiatingProcessFolderPath = tostring(todynamic(DeviceFileEvents_Details).InitiatingProcessFolderPath)
  | extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
  | extend HostNameDomain = iff(DeviceName != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
  | extend AccountName = tostring(split(UserId, "@")[0]), AccountUPNSuffix = tostring(split(UserId, "@")[1])
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: AADUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: InitiatingProcessFileName
      - identifier: Directory
        columnName: InitiatingProcessFolderPath
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

let Alerts and let CA_Alerts_Events are inlined into the numbered stages below.

Let Binding: CA_Events used in Stages 1, 12

let CA_Events = CloudAppEvents
| where ActionType == "FileDownloaded"
| extend parsed = parse_json(RawEventData)
| extend UserId = tostring(parsed.UserId)
| extend FileName = tostring(parsed.SourceFileName)
| extend FileExtension = tostring(parsed.SourceFileExtension)
| summarize CloudAppEvent_StartTime = min(TimeGenerated), CloudAppEvent_EndTime = max(TimeGenerated), CloudAppEvent_Files = make_set(FileName), FileCount = dcount(FileName) by Application, AccountObjectId, UserId, IPAddress, City, CountryCode
| extend CloudAppEvents_Details = pack_all();

Let Binding: CA_FileList used in Stage 3

let CA_FileList = CA_Alerts_Events | project CloudAppEvent_Files;

Stage 1: source

let CA_Events

Stage 2: source

let CA_Alerts_Events

Stage 3: source

let CA_FileList

Stage 4: source

SecurityAlert

Stage 5: where

where AlertName =~ "mass download by a single user"

Stage 6: where

where Status != "Resolved"

Stage 7: extend

extend accountEnt, ipEnt

Stage 8: kusto:mv-apply

kusto:mv-apply ipEnt.Type == "ip"

Stage 9: kusto:mv-apply

kusto:mv-apply accountEnt.Type == "account"

Stage 10: extend

extend Alert_TimeGenerated

Stage 11: distinct

distinct AADUserId, Alert_TimeGenerated, CompromisedEntity, Description, DisplayName, Entities, ExtendedProperties, IpAddress, ProductName, Status

Stage 12: join

join kind=inner (CA_Events)

Stage 13: where

where CloudAppEvent_EndTime <= Alert_TimeGenerated

Stage 14: project

project AADUserId, Alert_TimeGenerated, CloudAppEvent_Files, CloudAppEvents_Details, IPAddress, UserId

Stage 15: join

join kind=inner (DeviceFileEvents) on $left.UserId, $right.InitiatingProcessAccountUpn

Stage 16: where

where DeviceFileEvent_StartTime >= Alert_TimeGenerated

Stage 17: join

join kind=inner (DeviceEvents) on DeviceId

Stage 18: where

where USB_TimeGenerated >= Alert_TimeGenerated

Stage 19: mv-expand

mv-expand DeviceFileEvent_Files

Stage 20: extend

extend DeviceFileEvent_Files

Stage 21: where

where DeviceFileEvent_Files startswith "USB_DriveLetter"

Stage 22: summarize

summarize CloudAppEvents_Details, DeviceFileEvents_Details, USB_Details, USB_Drive_MatchedFiles by Alert_TimeGenerated, USB_TimeGenerated, UserId, AADUserId, DeviceId, DeviceName, IPAddress, CloudAppEvents_Details, DeviceFileEvents_Details, USB_Details

Stage 23: extend (4 consecutive steps)

extend AccountName, AccountUPNSuffix, DomainIndex, HostName, HostNameDomain, InitiatingProcessFileName, InitiatingProcessFolderPath

Stage 24: project-away

project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADUserIdsummarize
Alert_TimeGeneratedsummarize
CloudAppEvents_Detailssummarize
DeviceFileEvents_Detailssummarize
DeviceIdsummarize
DeviceNamesummarize
IPAddresssummarize
USB_Detailssummarize
USB_Drive_MatchedFilessummarize
USB_TimeGeneratedsummarize
UserIdsummarize
InitiatingProcessFileNameextend
InitiatingProcessFolderPathextend
HostNameextend
HostNameDomainextend
AccountNameextend
AccountUPNSuffixextend

McAfee ePO - Logging error occurred

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

Detects when logging errors on agent.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Telemetry Coverage

Rule Body

id: 0c9243d6-d2ec-48e1-8593-e713859c8f3c
name: McAfee ePO - Logging error occurred
description: |
  'Detects when logging errors on agent.'
severity: Medium
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1070
query: |
  McAfeeEPOEvent
  | where EventId in ('1040', '1076', '3032', '3033', '3034', '3036', '3038')
  | extend EventMessage = case(EventId == '1040', 'Activity Log error',
                                EventId == '1076', 'Error logging information',
                                EventId == '3032', 'Error while trying to open/create activity log file',
                                EventId == '3033', 'Activity log file maximum size reached',
                                EventId == '3034', 'Unable to write the activity log file',
                                EventId == '3036', 'Error during initialization of the activity log file',
                                'Error writing to log')
  | project DvcIpAddr, EventId, EventMessage
  | extend IPCustomEntity = DvcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where EventId in ('1040', '1076', '3032', '3033', '3034', '3036', '3038')

Stage 3: extend

| extend EventMessage = case(EventId == '1040', 'Activity Log error',
                              EventId == '1076', 'Error logging information',
                              EventId == '3032', 'Error while trying to open/create activity log file',
                              EventId == '3033', 'Activity log file maximum size reached',
                              EventId == '3034', 'Unable to write the activity log file',
                              EventId == '3036', 'Error during initialization of the activity log file',
                              'Error writing to log')
EventMessage =
ifEventId == 1040'Activity Log error'
elifEventId == 1076'Error logging information'
elifEventId == 3032'Error while trying to open/create activity log file'
elifEventId == 3033'Activity log file maximum size reached'
elifEventId == 3034'Unable to write the activity log file'
elifEventId == 3036'Error during initialization of the activity log file'
else'Error writing to log'

Stage 4: project

| project DvcIpAddr, EventId, EventMessage

Stage 5: extend

| extend IPCustomEntity = DvcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIdin
  • 1040
  • 1076
  • 3032
  • 3033
  • 3034
  • 3036
  • 3038
field:"EventId" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
DvcIpAddrproject
EventIdproject
EventMessageproject
IPCustomEntityextend

Mimecast Secure Email Gateway - Spam Event Thread

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

'Detects threat from spam event thread protection logs.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 0cda82c8-e8f0-4117-896e-a10f1b43e64a
name: Mimecast Secure Email Gateway - Spam Event Thread
description: |
  'Detects threat from spam event thread protection logs.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: MimecastSEGAPI
    dataTypes:
      - MimecastCG
enabled: true
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Discovery
relevantTechniques:
- T1083
query: |
  MimecastCG
  | where Type == "email_spam"
  | extend  SenderEnvelope = ['Sender Envelope'] 
suppressionDuration: 5h
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
- entityType: MailMessage
  fieldMappings:
  - identifier: Sender
    columnName: SenderEnvelope
  - identifier: Recipient
    columnName: Recipients
  - identifier: Subject
    columnName: Subject
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastCG

Stage 2: where

| where Type == "email_spam"

Stage 3: extend

| extend  SenderEnvelope = ['Sender Envelope']

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Typeeq
  • email_spam
field:"Type" kind:eq value:"email_spam"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SenderEnvelopeextend

Mimecast Secure Email Gateway - Spam Event Thread

#
Severity
low
Time window
15m
Source
github.com/Azure/Azure-Sentinel

Detects threat from spam event thread protection logs

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: df1b9377-5c29-4928-872f-9934a6b4f611
name: Mimecast Secure Email Gateway - Spam Event Thread
description: Detects threat from spam event thread protection logs
severity: Low
requiredDataConnectors:
  - connectorId: MimecastSIEMAPI
    dataTypes:
      - MimecastSIEM_CL
enabled: true
query: MimecastSIEM_CL| where mimecastEventId_s=="mail_spameventthread"
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- Discovery
relevantTechniques:
- T1083
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1d
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
customDetails:
  MsgId_s: MsgId_s
  headerFrom: headerFrom_s
  Route: Route_s
  SourceIP: SourceIP
  SenderDomain: SenderDomain_s
entityMappings:
- entityType: MailMessage
  fieldMappings:
  - identifier: Sender
    columnName: Sender_s
  - identifier: Recipient
    columnName: Recipient_s
  - identifier: Subject
    columnName: Subject_s
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastSIEM_CL

Stage 2: where

| where mimecastEventId_s=="mail_spameventthread"

Indicators

These rows show field, operator, and value matches.

Mimecast Targeted Threat Protection - Attachment Protect

#

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.

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

Detects a threat for an unsafe attachment in an email

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: aa75944c-a663-4901-969e-7b55bfa49a73
name: Mimecast Targeted Threat Protection - Attachment Protect
description: Detects a threat for an unsafe attachment in an email
severity: High
requiredDataConnectors:
  - connectorId: MimecastTTPAPI
    dataTypes:
      - MimecastTTPAttachment_CL
enabled: true
query: MimecastTTPAttachment_CL| where result_s <> "safe";
queryFrequency: 5m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- InitialAccess
- Discovery
relevantTechniques:
- T0865
alertRuleTemplateName:
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1d
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  fileHash: fileHash_s
  fileName: fileName_s
  fileType: fileType_s
  details: details_s
entityMappings:
  - entityType: MailMessage
    fieldMappings:
      - identifier: Sender
        columnName: senderAddress_s
      - identifier: Recipient
        columnName: recipientAddress_s
      - identifier: Subject
        columnName: subject_s
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastTTPAttachment_CL

Stage 2: where

| where result_s <> "safe";

Indicators

These rows show field, operator, and value matches.

NTLM Relay Attack

#
Group by
$left.RemoteIP, $left.ShortDeviceName, $right.RemoteDeviceName, $right.RemoteIP
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query searches for successful NTLM network logins where the device name contained in the NTLM authentication message contains a device that is known to MDE, but the source IP address is different from the known source IP address for that specific device. This could indicate an attacker is relaying the NTLM authentication information. To remove false positives, this query also searches for an outgoing network connection from the initiator to the attacker.

Known False Positives

  • None expected.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1d;
// Extract a list of known local IPs per device. Note that the DeviceNetworkEvents table is used for this, since this is faster than the
// DeviceNetworkInfo table where IP addresses are stored inside a JSON structure that requires additional parsing.
let DeviceIPs=(
    DeviceNetworkEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "ConnectionAttempt" or ActionType == "ConnectionSuccess"
    | distinct DeviceName, LocalIP
    | extend DeviceName=tolower(split(DeviceName,".")[0])
);
// Find potential NTLM relay attack by looking for NTLM logins from devices that are known in MDE, but are from a source IP that does not match any known IP addresses for the device.
let PotentialNTLMRelayLogins=materialize (
    DeviceLogonEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "LogonSuccess"
    | where LogonType == "Network"
    | where Protocol=="NTLM"
    | where isnotempty(RemoteDeviceName) and isnotempty(RemoteIP)
    | where RemoteIPType <> "Loopback"
    | extend RemoteDeviceName=tolower(RemoteDeviceName)
    | where RemoteDeviceName in ((DeviceIPs | project DeviceName)) // The remote device is known in MDE.
    | join kind=leftanti DeviceIPs on $left.RemoteIP == $right.LocalIP, $left.RemoteDeviceName == $right.DeviceName // The Remote IP does not match any known IP for the device.
    | project-reorder Timestamp, RemoteIP, RemoteDeviceName, AccountDomain, AccountName
);
// Filter the potential NTLM relay events by checking there was an outgoing SMB connection from the source device to the relay IP address.
DeviceNetworkEvents
| where ingestion_time() >= ago(timeframe)
| where RemotePort in (445, 80, 9389)
| where RemoteIP in ((PotentialNTLMRelayLogins | project RemoteIP))
| extend ShortDeviceName=tolower(split(DeviceName,".")[0])
| where ShortDeviceName in ((PotentialNTLMRelayLogins | project RemoteDeviceName))
| lookup kind=inner PotentialNTLMRelayLogins on $left.ShortDeviceName == $right.RemoteDeviceName, $left.RemoteIP == $right.RemoteIP
| extend HostName=tostring(split(DeviceName,".")[0]),DnsDomain=iif(DeviceName contains ".", substring(DeviceName, indexof(DeviceName, ".") + 1, strlen(DeviceName)),"")
// Begin environment-specific filter.
// End environment-specific filter.

Stages and Predicates

Parameters

let timeframe = 2*1d;

Let Binding: DeviceIPs used in Stage 1

let DeviceIPs = (
    DeviceNetworkEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "ConnectionAttempt" or ActionType == "ConnectionSuccess"
    | distinct DeviceName, LocalIP
    | extend DeviceName=tolower(split(DeviceName,".")[0])
);

Let Binding: PotentialNTLMRelayLogins used in Stages 2, 6, 8, 9

let PotentialNTLMRelayLogins = materialize (
    DeviceLogonEvents
    | where ingestion_time() >= ago(timeframe)
    | where ActionType == "LogonSuccess"
    | where LogonType == "Network"
    | where Protocol=="NTLM"
    | where isnotempty(RemoteDeviceName) and isnotempty(RemoteIP)
    | where RemoteIPType <> "Loopback"
    | extend RemoteDeviceName=tolower(RemoteDeviceName)
    | where RemoteDeviceName in ((DeviceIPs | project DeviceName))
    | join kind=leftanti DeviceIPs on $left.RemoteIP == $right.LocalIP, $left.RemoteDeviceName == $right.DeviceName
    | project-reorder Timestamp, RemoteIP, RemoteDeviceName, AccountDomain, AccountName
);

Stage 1: source

let DeviceIPs

Stage 2: source

let PotentialNTLMRelayLogins

Stage 3: source

DeviceNetworkEvents

Stage 4: where

where ...

Stage 5: where

where RemotePort in (445, 80, 9389)

Stage 6: where

where /* macro: (RemoteIP in (PotentialNTLMRelayLogins | project)) */

Stage 7: extend

extend ShortDeviceName

Stage 8: where

where /* macro: (ShortDeviceName in (PotentialNTLMRelayLogins | project)) */

Stage 9: kusto:lookup

lookup kind=inner (PotentialNTLMRelayLogins) on $left.ShortDeviceName, $right.RemoteDeviceName, $left.RemoteIP, $right.RemoteIP

Stage 10: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ShortDeviceNameextend
DnsDomainextend
HostNameextend

PaloAlto - Possible flooding

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

'Detects possible flooding.'

MITRE ATT&CK Coverage

Rule Body

id: feb185cc-51f4-11ec-bf63-0242ac130002
name: PaloAlto - Possible flooding
description: |
  'Detects possible flooding.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  PaloAltoCDLEvent
  | where EventResourceId =~ 'TRAFFIC'
  | where isnotempty(NetworkSessionId)
  | where DstBytes == 0 and tolong(NetworkPackets) > 0
  | extend AccountCustomEntity = DstUsername, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

PaloAltoCDLEvent

Stage 2: where

| where EventResourceId =~ 'TRAFFIC'

Stage 3: where

| where isnotempty(NetworkSessionId)

Stage 4: where

| where DstBytes == 0 and tolong(NetworkPackets) > 0

Stage 5: extend

| extend AccountCustomEntity = DstUsername, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

PROD (TM005.1) - GPO - Linked, Unlinked, or Enforced at Tier Level OU

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

Linking, Unlinking, or Enforcing of a GPO within the Tier Level OU structure

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 02cf2172-0eb7-4a4a-bce7-7251cdd3e233
name: 'PROD (TM005.1) - GPO - Linked, Unlinked, or Enforced at Tier Level OU'
description: |
  Linking, Unlinking, or Enforcing of a GPO within the Tier Level OU structure
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1484.001
query: |
  SecurityEvent 
  | where EventID == 5136 
      and (EventData matches regex @"(?i)OU=Domain Controllers"
          or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
          or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
          or EventData matches regex @"(?i)OU=Tier Model Administration"
          or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
      and Activity has "modified" 
      and EventData has '"ObjectClass">organizationalUnit'
      and EventData has '<Data Name="AttributeLDAPDisplayName">gPLink</Data>' 
      and (EventData has ';0]' 
          or EventData has ';1]' 
          or EventData has ';2]')
      and EventData has '"OperationType">%%14674'
  | extend ObjectName = extract("<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),(?i)DC=", 1, EventData)
  | extend ObjectTier = case(
      ObjectName has "Tier 0", "Tier 0",
      ObjectName has "Tier 1", "Tier 1",
      ObjectName has "Tier 2", "Tier 2",
      ObjectName has "Tier Model Administration", "Tier 0",
      ObjectName has "Tier Model Computer Quarantine", "Tier 0",
      ObjectName has "Domain Controllers", "Tier 0",
      "Unknown")
  | extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
  | extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
  | extend LastThreeChars = substring(AttributeValue, strlen(AttributeValue) - 2, 3)
  | extend AttributeValueDescription = case(
      LastThreeChars == ";0", "Linked",
      LastThreeChars == ";1", "Unlinked",
      LastThreeChars == ";2", "Enforced",
      "Unknown")
  | extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
  | extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)
  | project TimeGenerated
     , ObjectName
     , ObjectTier
     , ObjectClass
     , ActivityType = AttributeValueDescription
     , AttributeLDAP
     , Account
     , Domain
     , Computer
     , Channel
     , EventID
     , EventData
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ObjectName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Computer
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
customDetails:
  ObjectName: ObjectName
  ObjectTier: ObjectTier
  ObjectClass: ObjectClass
  ActivityType: ActivityType
  AttributeLDAP: AttributeLDAP
  Account: Account
  Domain: Domain
  Computer: Computer
  Channel: Channel
  EventID: EventID
  EventData: EventData
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: '(TM005.1) A GPO was {{ActivityType}} at the following {{ObjectTier}} {{ObjectName}} OU'
  alertDescriptionFormat: 'This will alert when a GPO is linked, unlinked, or enforced at any of the Tier Model OUs.'
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 5136 
    and (EventData matches regex @"(?i)OU=Domain Controllers"
        or EventData matches regex @"(?i)OU=Tier 0 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 1 Member Servers"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Accounts"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Devices"
        or EventData matches regex @"(?i)OU=Tier 2 End-User Groups"
        or EventData matches regex @"(?i)OU=Tier Model Administration"
        or EventData matches regex @"(?i)OU=Tier Model Computer Quarantine")
    and Activity has "modified" 
    and EventData has '"ObjectClass">organizationalUnit'
    and EventData has '<Data Name="AttributeLDAPDisplayName">gPLink</Data>' 
    and (EventData has ';0]' 
        or EventData has ';1]' 
        or EventData has ';2]')
    and EventData has '"OperationType">%%14674'

Stage 3: extend (8 consecutive steps)

| extend ObjectName = extract("<Data Name=\"ObjectDN\">(((?i)OU=[^,]+,?)+),(?i)DC=", 1, EventData)
| extend ObjectTier = case(
    ObjectName has "Tier 0", "Tier 0",
    ObjectName has "Tier 1", "Tier 1",
    ObjectName has "Tier 2", "Tier 2",
    ObjectName has "Tier Model Administration", "Tier 0",
    ObjectName has "Tier Model Computer Quarantine", "Tier 0",
    ObjectName has "Domain Controllers", "Tier 0",
    "Unknown")
| extend AttributeLDAP = extract("<Data Name=\"AttributeLDAPDisplayName\">([^<]+)</Data>", 1, EventData)
| extend AttributeValue = extract("\\[([^\\]]+)\\]", 1, EventData)
| extend LastThreeChars = substring(AttributeValue, strlen(AttributeValue) - 2, 3)
| extend AttributeValueDescription = case(
    LastThreeChars == ";0", "Linked",
    LastThreeChars == ";1", "Unlinked",
    LastThreeChars == ";2", "Enforced",
    "Unknown")
| extend ObjectClass = extract("<Data Name=\"ObjectClass\">([^<]+)</Data>", 1, EventData)
| extend Domain = extract("([^.]+\\.[^.]+)$", 1, Computer)

Stage 4: project

| project TimeGenerated
   , ObjectName
   , ObjectTier
   , ObjectClass
   , ActivityType = AttributeValueDescription
   , AttributeLDAP
   , Account
   , Domain
   , Computer
   , Channel
   , EventID
   , EventData

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activitymatch
  • modified transforms: term corpus 10 (kusto 10)
field:"Activity" kind:match value:"modified"
EventDatamatch
  • "ObjectClass">organizationalUnit transforms: term corpus 5 (kusto 5)
  • "OperationType">%%14674 transforms: term corpus 7 (kusto 7)
  • ;0] transforms: term corpus 4 (kusto 4)
  • ;1] transforms: term corpus 4 (kusto 4)
  • ;2] transforms: term corpus 4 (kusto 4)
  • <Data Name="AttributeLDAPDisplayName">gPLink</Data> transforms: term corpus 4 (kusto 4)
field:"EventData" kind:match
EventDataregex_match
  • (?i)OU=Domain Controllers corpus 8 (kusto 8)
  • (?i)OU=Tier 0 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 1 Member Servers corpus 8 (kusto 8)
  • (?i)OU=Tier 2 End-User Accounts corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Devices corpus 3 (kusto 3)
  • (?i)OU=Tier 2 End-User Groups corpus 3 (kusto 3)
  • (?i)OU=Tier Model Administration corpus 3 (kusto 3)
  • (?i)OU=Tier Model Computer Quarantine corpus 3 (kusto 3)
field:"EventData" kind:regex_match
EventIDeq
  • 5136 corpus 20 (kusto 18, elastic 2)
field:"EventID" kind:eq value:"5136"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ActivityTypeproject
AttributeLDAPproject
Channelproject
Computerproject
Domainproject
EventDataproject
EventIDproject
ObjectClassproject
ObjectNameproject
ObjectTierproject
TimeGeneratedproject

Remote Desktop Network Brute force (ASIM Network Session schema)

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

This detection identifies RDP application network traffic and filters any source/destination pair generating more than 25 events hard threshold.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: b7dc801e-1e79-48bb-91e8-2229a8e6d40b
name: Remote Desktop Network Brute force (ASIM Network Session schema)
description: |
  'This detection identifies RDP application network traffic and filters any source/destination pair generating more than 25 events hard threshold.'
severity: Medium
status: Available 
tags:
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // Start of the query and  Filter events that resulted in failure
  _Im_NetworkSession(eventresult="Failure")
  // Filter out private source IP addresses and focus on specific destination port (3389)
  // Also, ensure that the source and destination IP addresses are not the same
  | where not(ipv4_is_private(SrcIpAddr)) and tostring(DstPortNumber) has_any ("3389") and SrcIpAddr != DstIpAddr
  // Summarize the data by source and destination IP addresses, destination port number, network protocol, and event result
  // Also, bin the time generated in 5-minute intervals
  // Calculate the minimum and maximum time generated, the sum of event counts, and a set of up to 10 event vendors
  | summarize Starttime= min(TimeGenerated),EndTime= max(TimeGenerated),TargettedIPs=dcount(DstIpAddr),Eventscount=sum(EventCount),EventVendors=make_set(EventVendor,10) by SrcIpAddr,DstPortNumber,EventResult, bin(TimeGenerated, 5m)
  // Filter the summarized data to include only those with an event count of 25 or more
  | where Eventscount >= 25
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

_Im_NetworkSession(eventresult="Failure")

Stage 2: where

| where not(ipv4_is_private(SrcIpAddr)) and tostring(DstPortNumber) has_any ("3389") and SrcIpAddr != DstIpAddr

Stage 3: summarize

| summarize Starttime= min(TimeGenerated),EndTime= max(TimeGenerated),TargettedIPs=dcount(DstIpAddr),Eventscount=sum(EventCount),EventVendors=make_set(EventVendor,10) by SrcIpAddr,DstPortNumber,EventResult, bin(TimeGenerated, 5m)

Stage 4: where

| where Eventscount >= 25

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DstPortNumbermatch
  • 3389 transforms: tostring, term
field:"DstPortNumber" kind:match value:"3389"
Eventscountge
  • 25 transforms: number
field:"Eventscount" kind:ge value:"25"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SrcIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:SrcIpAddr

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstPortNumbersummarize
EndTimesummarize
EventResultsummarize
EventVendorssummarize
Eventscountsummarize
SrcIpAddrsummarize
Starttimesummarize
TargettedIPssummarize

SAP BTP - Audit log service unavailable

#
Status
available
Severity
high
Time window
7d
Group by
SubaccountName, Tenant
Source
github.com/Azure/Azure-Sentinel

Identifies SAP BTP subaccounts that have not reported audit logs for an unusual period. This could indicate that the audit log service has been disabled or tampered with, potentially by an attacker attempting to hide malicious activity. It may also indicate service key expiry or SAP BTP service availability problems.

MITRE ATT&CK Coverage

Rule Body

id: 8a3b5c7d-9e1f-4a2b-8c6d-3e5f7a9b1c2d
kind: Scheduled
name: SAP BTP - Audit log service unavailable
description: |
  Identifies SAP BTP subaccounts that have not reported audit logs for an unusual period.
  This could indicate that the audit log service has been disabled or tampered with,
  potentially by an attacker attempting to hide malicious activity. It may also indicate
  service key expiry or SAP BTP service availability problems.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.008
query: |
  // Configure the detection threshold (in minutes) - adjust based on your environment
  let detection_threshold_in_minutes = 60;
  // Lookback period to identify known subaccounts
  let lookback = 7d;
  // Get all known subaccounts and their last log time
  let last_activity = SAPBTPAuditLog_CL
      | where TimeGenerated > ago(lookback)
      | summarize LastLogTime = max(TimeGenerated) by SubaccountName, Tenant;
  // Identify subaccounts with no recent activity exceeding the threshold
  last_activity
  | where datetime_diff('minute', now(), LastLogTime) > detection_threshold_in_minutes
  | extend TimeSinceLastLog = datetime_diff('minute', now(), LastLogTime)
  | project 
      SubaccountName,
      Tenant,
      LastLogTime,
      TimeSinceLastLog,
      CloudApp = "SAP BTP"
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP BTP: No audit logs received from {{SubaccountName}} for {{TimeSinceLastLog}} minutes'
  alertDescriptionFormat: |
    The SAP BTP subaccount '{{SubaccountName}}' has not reported any audit logs since {{LastLogTime}}.
    
    Time without logs: {{TimeSinceLastLog}} minutes
    
    This could indicate:
    - Audit log service has been disabled (potential compromise to hide malicious activity)
    - Data connector authentication or connectivity issues
    - SAP BTP service availability problems
    
    Recommended actions:
    1. Verify the audit log service status in SAP BTP cockpit
    2. Check the data connector health in Microsoft Sentinel
    3. Review any recent administrative changes to the subaccount
    4. Investigate for potential unauthorized access or configuration changes
customDetails:
  SubaccountName: SubaccountName
  Tenant: Tenant
  LastLogTime: LastLogTime
  TimeSinceLastLog: TimeSinceLastLog
version: 1.0.1

Stages and Predicates

Parameters

let detection_threshold_in_minutes = 60;
let lookback = 7d;

let last_activity is inlined into the numbered stages below.

Stages 1 to 3 define let last_activity as the rule's main pipeline source. Stages 4 to 6 use it.

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: where

| where TimeGenerated > ago(lookback)

Stage 3: summarize

| summarize LastLogTime = max(TimeGenerated) by SubaccountName, Tenant

Stage 4: where

last_activity
| where datetime_diff('minute', now(), LastLogTime) > detection_threshold_in_minutes

Stage 5: extend

| extend TimeSinceLastLog = datetime_diff('minute', now(), LastLogTime)

Stage 6: project

| project 
    SubaccountName,
    Tenant,
    LastLogTime,
    TimeSinceLastLog,
    CloudApp = "SAP BTP"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CloudAppproject
LastLogTimeproject
SubaccountNameproject
Tenantproject
TimeSinceLastLogproject

SonicWall - Capture ATP Malicious File Detection

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

'This rule identifies malicious file verdicts from the SonicWall Capture ATP service. This analytic rule leverages the SonicWall Firewall ASIM Network Session parser (ASimNetworkSessionSonicWallFirewall). Ref: https://www.sonicwall.com/products/capture-advanced-threat-protection/ Ref: https://www.sonicwall.com/support/knowledge-base/how-to-view-threat-reports-capture-atp/170505384715913/'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: 3db9f99e-a459-41e0-8e02-8b332f5fcb2c
name: SonicWall - Capture ATP Malicious File Detection
description: |
  'This rule identifies malicious file verdicts from the SonicWall Capture ATP service. This analytic rule leverages the SonicWall Firewall ASIM Network Session parser (ASimNetworkSessionSonicWallFirewall).
    Ref: https://www.sonicwall.com/products/capture-advanced-threat-protection/
    Ref: https://www.sonicwall.com/support/knowledge-base/how-to-view-threat-reports-capture-atp/170505384715913/'
severity: Medium
status: Experimental
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  CommonSecurityLog
  | where DeviceEventClassID == 1631
  | extend CaptureATPVerdict = extract(@'Gateway Anti-Virus Status: (.*)\. ', 1, Message)
  | where CaptureATPVerdict == "BAD"
  | parse-kv AdditionalExtensions as (['susr']:string, ['fileid']:string) with (pair_delimiter=";", kv_delimiter="=")
  | extend
      NetworkProtocol = toupper(iff(Protocol contains "-" and Protocol !contains "/", toupper(trim_start(@".*-", Protocol)), toupper(trim_end(@"/.*", Protocol)))),
      NetworkApplicationProtocol = tostring(toupper(trim_start(@".*/", Protocol)))
  | project
      TimeGenerated,
      DeviceModel = DeviceProduct,
      SerialNumber = Computer,
      SrcIpAddr = SourceIP,
      SrcUsername = coalesce(susr, SourceUserName),
      NetworkProtocol,
      NetworkApplicationProtocol,
      RequestURL,
      File = url_decode(tostring(split(RequestURL, '/')[-1])),
      Hash = fileid,
      Verdict = CaptureATPVerdict
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: File
  - entityType: Url
    fieldMappings:
      - identifier: Url
        columnName: RequestURL
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUsername
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceEventClassID == 1631

Stage 3: extend

| extend CaptureATPVerdict = extract(@'Gateway Anti-Virus Status: (.*)\. ', 1, Message)

Stage 4: where

| where CaptureATPVerdict == "BAD"

Stage 5: parse

| parse-kv AdditionalExtensions as (['susr']:string, ['fileid']:string) with (pair_delimiter=";", kv_delimiter="=")

Stage 6: extend

| extend
    NetworkProtocol = toupper(iff(Protocol contains "-" and Protocol !contains "/", toupper(trim_start(@".*-", Protocol)), toupper(trim_end(@"/.*", Protocol)))),
    NetworkApplicationProtocol = tostring(toupper(trim_start(@".*/", Protocol)))

Stage 7: project

| project
    TimeGenerated,
    DeviceModel = DeviceProduct,
    SerialNumber = Computer,
    SrcIpAddr = SourceIP,
    SrcUsername = coalesce(susr, SourceUserName),
    NetworkProtocol,
    NetworkApplicationProtocol,
    RequestURL,
    File = url_decode(tostring(split(RequestURL, '/')[-1])),
    Hash = fileid,
    Verdict = CaptureATPVerdict

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceModelproject
Fileproject
Hashproject
NetworkApplicationProtocolproject
NetworkProtocolproject
RequestURLproject
SerialNumberproject
SrcIpAddrproject
SrcUsernameproject
TimeGeneratedproject
Verdictproject

Suspicious TGT Request with a DC Account

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

Below query detects TGT requests from a DC account with an IP that doesn't belong to a DC. It detect PetitPotam and any other attacks that uses a stolen DC certificate/account to perform operations.
If you make it a detection rule, take ingestion delay into account.

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique

Telemetry Coverage

Rule Body

// Author       : Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
//
// Link to original post:
// https://posts.bluraven.io/detecting-petitpotam-and-other-domain-controller-account-takeovers-d3364bd9ee0a
//
// Description  : This query detects if a computer account of a Domain Controller is stolen and used from a Non-DC device. 
//                computer account of a DC can bu used to obtain a TGT.
//
// Query parameters:
//
// list of DCs
let DCs = dynamic(["yourdc1.yourdomain.local"]);
// list of DC IPs
let DC_IPs = dynamic(["IP of the DCs including IPv6 addresses"]);
//
SecurityEvent
| where EventID == 4768
| where Computer in~ (DCs)
| where TargetUserName endswith "$"
| where DCs has replace_string(TargetUserName,"$","")
| where IpAddress <> "::1"
| extend IpAddress = replace_string(IpAddress, "::ffff:", "")
| where IpAddress !in (DC_IPs)
| project-reorder Computer, TargetUserName, IpAddress

Stages and Predicates

Parameters

let DCs = dynamic(["yourdc1.yourdomain.local"]);
let DC_IPs = dynamic(["IP of the DCs including IPv6 addresses"]);

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 4768

Stage 3: where

| where Computer in~ (DCs)

Stage 4: where

| where TargetUserName endswith "$"

Stage 5: where

| where DCs has replace_string(TargetUserName,"$","")

Stage 6: where

| where IpAddress <> "::1"

Stage 7: extend

| extend IpAddress = replace_string(IpAddress, "::ffff:", "")

Stage 8: where

| where IpAddress !in (DC_IPs)

Stage 9: project-reorder

| project-reorder Computer, TargetUserName, IpAddress

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IpAddresseqDC_IPsexcludes:IpAddress field:"IpAddress" value:"DC_IPs"

Output Fields

These fields are emitted when the rule matches.

FieldSource
IpAddressextend

Theom - Financial data exposed

#

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
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0026 (Theom has observed financial data in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"

MITRE ATT&CK Coverage

Rule Body

id: 0cead100-f6ca-4cbb-989d-424d20705f30
name: Theom - Financial data exposed
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0026 (Theom has observed financial data in a data store that is publicly exposed. As per this requirement, use this information to apply data access control lists or access permissions to secure your data)"
severity: High
status : Available
requiredDataConnectors: 
  - connectorId: Theom
    dataTypes:
      - TheomAlerts_CL
queryPeriod: 5m
queryFrequency: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
  - T1530
query: |
   TheomAlerts_CL
     | where customProps_RuleId_s == "TRIS0026" and (priority_s == "P1" or priority_s == "P2")
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "Theom Alert ID: {{id_s}} "
  alertDescriptionFormat: "\nSummary: {{summary_s}}  \nAdditional info: {{details_s}}\nPlease investigate further on Theom UI at {{deepLink_s}}\n"
entityMappings: 
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: customProps_AssetName_s
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: deepLink_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

TheomAlerts_CL

Stage 2: where

| where customProps_RuleId_s == "TRIS0026" and (priority_s == "P1" or priority_s == "P2")

Indicators

These rows show field, operator, and value matches.

ThreatConnect TI map Email entity to SigninLogs

#
Severity
medium
Time window
14d
Group by
$left.EmailSenderAddress, $right.UserPrincipalName, IndicatorId, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies a match in SigninLogs table from any Email IOC from ThreatConnect TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: ecb68ce7-c309-59a7-a8de-07ccf2a0ea4f
name: ThreatConnect TI map Email entity to SigninLogs
version: 1.2.5
kind: Scheduled
description: |
  'Identifies a match in SigninLogs table from any Email IOC from ThreatConnect TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  let aadFunc = (tableName:string){
  ThreatIntelligenceIndicator
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where ExpirationDateTime > now() and Active == true
  // Filter out non ThreatConnect TI Sources
  | where SourceSystem startswith "ThreatConnect-"
  //Filtering the table for Email related IOCs
  | where isnotempty(EmailSenderAddress)
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (
      table(tableName) | where TimeGenerated >= ago(dt_lookBack) and isnotempty(UserPrincipalName)
      //Normalizing the column to lower case for exact match with EmailSenderAddress column
      | extend UserPrincipalName = tolower(UserPrincipalName)
      | where UserPrincipalName matches regex emailregex
      | extend Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
      | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
      | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
      // renaming timestamp column so it is clear the log this came from SigninLogs table
      | extend SigninLogs_TimeGenerated = TimeGenerated, Type = Type
  )
  on $left.EmailSenderAddress == $right.UserPrincipalName
  | where SigninLogs_TimeGenerated < ExpirationDateTime
  | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, UserPrincipalName
  | project SigninLogs_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, IPAddress, UserPrincipalName, AppDisplayName,
  StatusCode, StatusDetails, NetworkIP, NetworkDestinationIP, NetworkSourceIP, Type
  | extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
  | extend timestamp = SigninLogs_TimeGenerated
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName:string){
ThreatIntelligenceIndicator
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where ExpirationDateTime > now() and Active == true
| where SourceSystem startswith "ThreatConnect-"
| where isnotempty(EmailSenderAddress)
| join kind=innerunique (
    table(tableName) | where TimeGenerated >= ago(dt_lookBack) and isnotempty(UserPrincipalName)
    | extend UserPrincipalName = tolower(UserPrincipalName)
    | where UserPrincipalName matches regex emailregex
    | extend Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
    | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
    | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
    | extend SigninLogs_TimeGenerated = TimeGenerated, Type = Type
)
on $left.EmailSenderAddress == $right.UserPrincipalName
| where SigninLogs_TimeGenerated < ExpirationDateTime
| summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by IndicatorId, UserPrincipalName
| project SigninLogs_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, IPAddress, UserPrincipalName, AppDisplayName,
StatusCode, StatusDetails, NetworkIP, NetworkDestinationIP, NetworkSourceIP, Type
| extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
| extend timestamp = SigninLogs_TimeGenerated
};

Union isfuzzy=true (2 Sources)

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

Leg 1: aadSignin

Leg 2: aadNonInt

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppDisplayNameproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IPAddressproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
SigninLogs_TimeGeneratedproject
StatusCodeproject
StatusDetailsproject
ThreatTypeproject
Typeproject
Urlproject
UserPrincipalNameproject
Nameextend
UPNSuffixextend
timestampextend

TI Map IP entity to AzureFirewall

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.RemoteIP, Id, ObservableValue, RemoteIP
Source
github.com/Azure/Azure-Sentinel

Identifies a match in AzureFirewall (NetworkRule & ApplicationRule Logs) from any IP IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 4992d2f3-d6c0-4271-adac-b23532ba4492
name: TI Map IP entity to AzureFirewall
description: |
  Identifies a match in AzureFirewall (NetworkRule & ApplicationRule Logs) from any IP IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureFirewall
    dataTypes: 
      - AzureDiagnostics
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelIndicators
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
       | extend NetworkSourceIP = toupper(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    // Filter out indicators without relevant IP address fields
    | where TimeGenerated >= ago(ioc_lookBack)
    // Select the IP entity based on availability of different IP fields
    | extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    // Exclude local addresses using the ipv4_is_private operator and filtering out specific address prefixes
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
  // Perform a join between IP indicators and AzureDiagnostics logs to identify potential malicious activity
  IP_Indicators
     | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        union AZFWApplicationRule, AZFWNetworkRule
        | where TimeGenerated >= ago(dt_lookBack)
        //| where OperationName in ("AzureFirewallApplicationRuleLog", "AzureFirewallNetworkRuleLog")
        | parse kind=regex flags=U ActionReason with Protocol 'request from ' SourceHost 'to ' DestinationHost @'\.? Action: ' Firewall_Action @'\.' Rest_msg
        | extend SourceAddress = SourceIp
        | extend DestinationAddress = DestinationIp
        | extend RemoteIP = case(not(ipv4_is_private(DestinationAddress)), DestinationAddress, not(ipv4_is_private(SourceAddress)), SourceAddress, "")
        | where isnotempty(RemoteIP) // Filter out traffic involving public addresses only
        | extend Firewall_Action = Action
        | project-rename AzureFirewall_TimeGenerated = TimeGenerated
    )
    on $left.TI_ipEntity == $right.RemoteIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where AzureFirewall_TimeGenerated < ValidUntil
    // Group the results by IndicatorId and RemoteIP, and keep the log entry with the latest timestamp
    | summarize AzureFirewall_TimeGenerated = arg_max(AzureFirewall_TimeGenerated, *) by Id, RemoteIP
    // Select the desired output fields
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project LatestIndicatorTime, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
      AzureFirewall_TimeGenerated, TI_ipEntity, ActionReason, SourceAddress, DestinationAddress, Firewall_Action, Protocol,
      NetworkSourceIP, Type, TargetUrl
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: TI_ipEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: TargetUrl
version: 1.3.8
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

Stages 1 to 11 define let IP_Indicators as the rule's main pipeline source. Stages 12 to 18 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")

Stage 4: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend

| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
TI_ipEntity =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: extend

| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =
ifisempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIP
elseTI_ipEntity

Stage 9: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 10: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 11: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 12: project-reorder

IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity

Stage 13: join

join kind=innerunique (...) on $left.TI_ipEntity, $right.RemoteIP

Stage 14: where

where AzureFirewall_TimeGenerated < ValidUntil

Stage 15: summarize

summarize AzureFirewall_TimeGenerated by Id, RemoteIP

Stage 16: extend

extend Description

Stage 17: extend

extend ActivityGroupNames

Stage 18: project

project ActionReason, ActivityGroupNames, AzureFirewall_TimeGenerated, Confidence, Description, DestinationAddress, Firewall_Action, Id, LatestIndicatorTime, NetworkSourceIP, Protocol, SourceAddress, TI_ipEntity, TargetUrl, Type, ValidUntil

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
RemoteIPis_not_null
  • (no value, null check)
field:"RemoteIP" kind:is_not_null
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionReasonproject
ActivityGroupNamesproject
AzureFirewall_TimeGeneratedproject
Confidenceproject
Descriptionproject
DestinationAddressproject
Firewall_Actionproject
Idproject
LatestIndicatorTimeproject
NetworkSourceIPproject
Protocolproject
SourceAddressproject
TI_ipEntityproject
TargetUrlproject
Typeproject
ValidUntilproject

TI Map IP Entity to VMConnection

#
Severity
medium
Time window
14d
Group by
$left.TI_ipEntity, $right.RemoteIp, IndicatorId, RemoteIp
Source
github.com/Azure/Azure-Sentinel

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in VMConnection.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 9713e3c0-1410-468d-b79e-383448434b2d
name: TI Map IP Entity to VMConnection
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in VMConnection.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for VMConnection events
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelligenceIndicator
    | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
    | where Active == true and ExpirationDateTime > now();
  // Perform a join between IP indicators and VMConnection events
  IP_Indicators
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        VMConnection
        | where TimeGenerated >= ago(dt_lookBack)
        | extend VMConnection_TimeGenerated = TimeGenerated
    )
    on $left.TI_ipEntity == $right.RemoteIp
    // Filter out VMConnection events that occurred after the expiration of the corresponding indicator
    | where VMConnection_TimeGenerated < ExpirationDateTime
    // Group the results by IndicatorId and keep the VMConnection event with the latest timestamp
    | summarize VMConnection_TimeGenerated = arg_max(VMConnection_TimeGenerated, *) by IndicatorId, RemoteIp
    // Select the desired output fields
    | project VMConnection_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
      TI_ipEntity, Computer, Direction, ProcessName, SourceIp, DestinationIp, RemoteIp, Protocol, DestinationPort, NetworkIP, NetworkDestinationIP, NetworkSourceIP, EmailSourceIpAddress, Type
    | extend timestamp = VMConnection_TimeGenerated, HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: RemoteIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.2
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity)
TI_ipEntity =
ifisnotempty(NetworkIP)NetworkIP
elseNetworkDestinationIP

Stage 5: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 6: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId

Stage 7: where

| where Active == true and ExpirationDateTime > now()

Stage 8: join

IP_Indicators
| join kind=innerunique (
      VMConnection
      | where TimeGenerated >= ago(dt_lookBack)
      | extend VMConnection_TimeGenerated = TimeGenerated
  )
  on $left.TI_ipEntity == $right.RemoteIp

Stage 9: where

where VMConnection_TimeGenerated < ExpirationDateTime

Stage 10: summarize

summarize VMConnection_TimeGenerated by IndicatorId, RemoteIp

Stage 11: project

project ActivityGroupNames, Computer, ConfidenceScore, Description, DestinationIp, DestinationPort, Direction, EmailSourceIpAddress, ExpirationDateTime, IndicatorId, NetworkDestinationIP, NetworkIP, NetworkSourceIP, ProcessName, Protocol, RemoteIp, SourceIp, TI_ipEntity, ThreatType, Type, Url, VMConnection_TimeGenerated

Stage 12: extend

extend DnsDomain, HostName, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
Computerproject
ConfidenceScoreproject
Descriptionproject
DestinationIpproject
DestinationPortproject
Directionproject
EmailSourceIpAddressproject
ExpirationDateTimeproject
IndicatorIdproject
NetworkDestinationIPproject
NetworkIPproject
NetworkSourceIPproject
ProcessNameproject
Protocolproject
RemoteIpproject
SourceIpproject
TI_ipEntityproject
ThreatTypeproject
Typeproject
Urlproject
VMConnection_TimeGeneratedproject
DnsDomainextend
HostNameextend
timestampextend

Back to all Kusto rules