Detection rules › Kusto

Kusto rules: 0m

AWSCloudTrail - Privilege escalation via Glue policy

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

Detects inline IAM policy updates that grant broad AWS Glue permissions or Glue actions paired with IAM privileges such as iam:PassRole. This pattern can enable elevated execution paths and indicates potential privilege escalation through expanded cloud role capabilities.

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 370f0e5e-da1d-4a14-8ced-d1d7ab66a8d7
name: AWSCloudTrail - Privilege escalation via Glue policy
description: |
  Detects inline IAM policy updates that grant broad AWS Glue permissions or Glue actions paired with IAM
  privileges such as iam:PassRole. This pattern can enable elevated execution paths and indicates potential
  privilege escalation through expanded cloud role capabilities.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1098.003
query: |
    AWSCloudTrail
      | where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)
      | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
      | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
      | mvexpand Statement
      | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
      | extend Action = tostring(Action)
      | where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:CreateDevEndpoint" and Action contains "glue:GetDevEndpoints") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:Create*" and Action contains "glue:Get*")) or (Action contains "glue:*") or (Action contains "glue:GetDevEndpoints" and Action contains "glue:UpdateDevEndpoint") or (Action contains "glue:Get*" and Action contains "glue:Update*")) and Resource == "*" and Condition == ""
      | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
      | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
      | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
      | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
        AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
      | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Glue privilege escalation policy update by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} Event, updating inline Glue escalation policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("PutUserPolicy","PutRolePolicy","PutGroupPolicy") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 4: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 5: mv-expand

| mvexpand Statement

Stage 6: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)

Stage 7: extend

| extend Action = tostring(Action)

Stage 8: where

| where Effect =~ "Allow" and ((((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:CreateDevEndpoint" and Action contains "glue:GetDevEndpoints") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "glue:Create*" and Action contains "glue:Get*")) or (Action contains "glue:*") or (Action contains "glue:GetDevEndpoints" and Action contains "glue:UpdateDevEndpoint") or (Action contains "glue:Get*" and Action contains "glue:Update*")) and Resource == "*" and Condition == ""

Stage 9: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 10: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • glue:*
  • glue:Create*
  • glue:CreateDevEndpoint
  • glue:Get*
  • glue:GetDevEndpoints
  • glue:Update*
  • glue:UpdateDevEndpoint
  • iam:*
  • iam:PassRole
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • PutGroupPolicy corpus 12 (kusto 11, panther 1)
  • PutRolePolicy corpus 12 (kusto 11, panther 1)
  • PutUserPolicy corpus 12 (kusto 11, panther 1)
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
PolicyNameextend
Statementextend
Actionextend
Conditionextend
Effectextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Large Default Groups With Add Member Privileges

#

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 Add Member Privileges

Rule Body

id: 36262b9d-fc92-47fb-8ec1-bda6e74e3d4e
name: BloodHound Attack Path Finding - Large Default Groups With Add Member Privileges
description: BloodHound Attack Path Finding - Large Default Groups With Add Member
  Privileges
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 == "LargeDefaultGroupsAddMember"
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 == "LargeDefaultGroupsAddMember"

Indicators

These rows show field, operator, and value matches.

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

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC6 Scenario B Privileges

#

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 - Non-Tier Zero Principals With ADCS ESC6 Scenario B Privileges

Rule Body

id: 3050426c-56c0-4a20-9bb2-9d63d3c59484
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC6 Scenario
  B Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC6
  Scenario B Privileges
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 == "T0ADCSESC6b"
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 == "T0ADCSESC6b"

Indicators

These rows show field, operator, and value matches.

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

Cisco ASA - Possible Data Exfiltration Detection

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

'Detects potential data exfiltration when an internal source IP sends a large amount of outbound data to the internet, especially when the volume is significantly higher than its normal behavior.'

MITRE ATT&CK Coverage

Rule Body

id: f3a8c2d0-7b41-4e9a-9f6a-2d8a1c4e5b72
name: Cisco ASA - Possible Data Exfiltration Detection
description: |
  'Detects potential data exfiltration when an internal source IP sends a large amount of outbound data to the internet, especially when the volume is significantly higher than its normal behavior.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1041
  - T1048
query: |
  let Lookback = 1h;
  let MinUploadBytes = 1073741824;
  let MinUploadRatio = 95.0;
  CommonSecurityLog
  | where TimeGenerated > ago(Lookback)
  | where DeviceProduct has_any ("ASA")
  | where isnotempty(SourceIP) and isnotempty(DestinationIP)
  | where ipv4_is_private(SourceIP)
  | where not(ipv4_is_private(DestinationIP))
  | where DeviceAction in~ ("allow", "allowed", "accept", "accepted", "permitted")
  | extend SentBytesLong = tolong(SentBytes)
  | extend ReceivedBytesLong = tolong(ReceivedBytes)
  | where isnotnull(SentBytesLong)
  | summarize
      TotalBytesSent = sum(SentBytesLong),
      TotalBytesReceived = sum(coalesce(ReceivedBytesLong, 0)),
      Connections = count(),
      DestinationPorts = make_set(DestinationPort, 20),
      Applications = make_set(ApplicationProtocol, 20),
      FirstSeen = min(TimeGenerated),
      LastSeen = max(TimeGenerated)
      by SourceIP, DestinationIP
  | where TotalBytesSent >= MinUploadBytes
  | extend TotalGBSent = round(TotalBytesSent / 1024.0 / 1024.0 / 1024.0, 2)
  | extend TotalGBReceived = round(TotalBytesReceived / 1024.0 / 1024.0 / 1024.0, 2)
  | extend UploadRatio = round(todouble(TotalBytesSent) / todouble(TotalBytesSent + TotalBytesReceived) * 100, 2)
  | where UploadRatio >= MinUploadRatio
  | project
      TimeGenerated = LastSeen,
      SourceIP,
      DestinationIP,
      TotalGBSent,
      TotalGBReceived,
      UploadRatio,
      Connections,
      DestinationPorts,
      Applications,
      FirstSeen,
      LastSeen
  | order by TotalGBSent desc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let Lookback = 1h;
let MinUploadBytes = 1073741824;
let MinUploadRatio = 95.0;

Stage 1: source

CommonSecurityLog

Stage 2: where

| where TimeGenerated > ago(Lookback)

Stage 3: where

| where DeviceProduct has_any ("ASA")

Stage 4: where

| where isnotempty(SourceIP) and isnotempty(DestinationIP)

Stage 5: where

| where ipv4_is_private(SourceIP)

Stage 6: where

| where not(ipv4_is_private(DestinationIP))

Stage 7: where

| where DeviceAction in~ ("allow", "allowed", "accept", "accepted", "permitted")

Stage 8: extend

| extend SentBytesLong = tolong(SentBytes)

Stage 9: extend

| extend ReceivedBytesLong = tolong(ReceivedBytes)

Stage 10: where

| where isnotnull(SentBytesLong)

Stage 11: summarize

| summarize
    TotalBytesSent = sum(SentBytesLong),
    TotalBytesReceived = sum(coalesce(ReceivedBytesLong, 0)),
    Connections = count(),
    DestinationPorts = make_set(DestinationPort, 20),
    Applications = make_set(ApplicationProtocol, 20),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated)
    by SourceIP, DestinationIP

Stage 12: where

| where TotalBytesSent >= MinUploadBytes

Stage 13: extend (3 consecutive steps)

| extend TotalGBSent = round(TotalBytesSent / 1024.0 / 1024.0 / 1024.0, 2)
| extend TotalGBReceived = round(TotalBytesReceived / 1024.0 / 1024.0 / 1024.0, 2)
| extend UploadRatio = round(todouble(TotalBytesSent) / todouble(TotalBytesSent + TotalBytesReceived) * 100, 2)

Stage 14: where

| where UploadRatio >= MinUploadRatio

Stage 15: project

| project
    TimeGenerated = LastSeen,
    SourceIP,
    DestinationIP,
    TotalGBSent,
    TotalGBReceived,
    UploadRatio,
    Connections,
    DestinationPorts,
    Applications,
    FirstSeen,
    LastSeen

Stage 16: sort

| order by TotalGBSent desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DestinationIPis_not_null
  • (no value, null check)
field:"DestinationIP" kind:is_not_null
DeviceActionin
  • accept
  • accepted
  • allow
  • allowed
  • permitted
field:"DeviceAction" kind:in
DeviceProductmatch
  • ASA transforms: term
field:"DeviceProduct" kind:match value:"ASA"
SentBytesLongis_not_null
  • (no value, null check)
field:"SentBytesLong" kind:is_not_null
SourceIPcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"SourceIP" kind:cidr_match
SourceIPis_not_null
  • (no value, null check)
field:"SourceIP" kind:is_not_null
TotalBytesSentge
  • 1073741824 transforms: number
field:"TotalBytesSent" kind:ge value:"1073741824"
UploadRatioge
  • 95.0 transforms: number
field:"UploadRatio" kind:ge value:"95.0"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationIPcidr_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:DestinationIP

Output Fields

These fields are emitted when the rule matches.

FieldSource
Applicationsproject
Connectionsproject
DestinationIPproject
DestinationPortsproject
FirstSeenproject
LastSeenproject
SourceIPproject
TimeGeneratedproject
TotalGBReceivedproject
TotalGBSentproject
UploadRatioproject

Cloudflare - Unexpected client request

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

'Detects client requests to unusual client request.'

MITRE ATT&CK Coverage

Rule Body

id: f32142b1-4bcb-45c0-92e4-2ddc18768523
name: Cloudflare - Unexpected client request
description: |
  'Detects client requests to unusual client request.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CloudflareDataConnector
    dataTypes:
      - Cloudflare
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  Cloudflare
  | where HttpRequestMethod =~ 'GET'
  | where DstBytes != 0 or SrcBytes != 0
  | where ClientRequestURI has_any ('/admin', '/admin.php', 'wp-admin', '.htaccess', '/etc/shadow', '/etc/passwd', '/etc/hosts', '/etc/ssh/')
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

Cloudflare

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: where

| where DstBytes != 0 or SrcBytes != 0

Stage 4: where

| where ClientRequestURI has_any ('/admin', '/admin.php', 'wp-admin', '.htaccess', '/etc/shadow', '/etc/passwd', '/etc/hosts', '/etc/ssh/')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ClientRequestURImatch
  • .htaccess transforms: term
  • /admin transforms: term
  • /admin.php transforms: term
  • /etc/hosts transforms: term
  • /etc/passwd transforms: term
  • /etc/shadow transforms: term
  • /etc/ssh/ transforms: term
  • wp-admin transforms: term
field:"ClientRequestURI" kind:match
DstBytesne
  • 0
field:"DstBytes" kind:ne value:"0"
HttpRequestMethodeq
  • GET
field:"HttpRequestMethod" kind:eq value:"GET"
SrcBytesne
  • 0
field:"SrcBytes" kind:ne value:"0"

Detect Suspicious ncrypt.dll usage on admin device with RDP connections to non TPM protected device

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

This detection rule uses a WDAC audit policy to ingest missing DeviceImageLoad events in MDE, and check for suspicious processes using the ncrypt.dll and admin devices performing RDP connection to unmanaged or non-TPM devices. More information on the attack scenario this is detection is applicable for can be found in the references.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let time_lookback = 30d;
let admin_users = toscalar(
    IdentityInfo
    | where Timestamp > ago(7d)
    | where CriticalityLevel != "" or AccountDisplayName contains "Admin"
    | summarize make_set(AccountDisplayName)
);
let devices_with_admin_accounts = (
    ExposureGraphEdges
    // Get edges where source is a device and destination is a admin user
    | where SourceNodeLabel == "device" and TargetNodeLabel == "user"
    | where TargetNodeName in (admin_users)
    // Check which devices have the credentials of the admin user
    | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
    | graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
        project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
    | summarize make_set(IncomingNodeName)
);
let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    // Only flag admin devices
    | where DeviceName in (devices_with_admin_accounts)
    // Exclude MDI RDP Connections (known for NNR)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    // Search for RDP connections to non-tpm devices
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = Timestamp
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where Timestamp > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where Timestamp between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project Timestamp, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 
    InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
    NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
    RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported

let time_lookback = 30d;
let admin_users = toscalar(
    IdentityInfo
    | where TimeGenerated > ago(7d)
    | where CriticalityLevel != "" or AccountDisplayName contains "Admin"
    | summarize make_set(AccountDisplayName)
);
let devices_with_admin_accounts = (
    ExposureGraphEdges
    // Get edges where source is a device and destination is a admin user
    | where SourceNodeLabel == "device" and TargetNodeLabel == "user"
    | where TargetNodeName in (admin_users)
    // Check which devices have the credentials of the admin user
    | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
    | graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
        project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
    | summarize make_set(IncomingNodeName)
);
let no_tpm_devices = (
    ExposureGraphNodes
    // Get device nodes with their inventory ID
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    // Get interesting properties
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    // Search for distinct devices
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    // Get Unmanaged devices and device not supporting a TPM
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);
let no_tpm_device_info = (
    DeviceNetworkInfo
    | where TimeGenerated > ago(7d)
    // Get latest network info for each device ID
    | summarize arg_max(TimeGenerated, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    // Find no TPM devices and join with their network information
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);
let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where TimeGenerated > ago(time_lookback)
    // Only flag admin devices
    | where DeviceName in (devices_with_admin_accounts)
    // Exclude MDI RDP Connections (known for NNR)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    // Search for RDP connections to non-tpm devices
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);
// Get all possible nonce requests
let nonce_requests = (
    DeviceNetworkEvents
    | where TimeGenerated > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = TimeGenerated
);
// Get suspicious ncrypt.dll usage via WDAC audit policy
DeviceEvents
| where TimeGenerated > ago(time_lookback)
// Only flag admin devices
| where DeviceName in (devices_with_admin_accounts)
| where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"
// Check if the same initiating process is doing a nonce request
| join kind=inner nonce_requests on InitiatingProcessId, DeviceId
// Only flag when nonce was request 10min before of after ncrypt usage
| where TimeGenerated between (todatetime(NonceRequestTimestamp - 10m) .. todatetime(NonceRequestTimestamp + 10m))
// Check if the same device is doing RDP Connections
| join kind=inner dangerous_rdp_sessions on DeviceId
// Whitelist known good processes
| where InitiatingProcessFileName !in ("backgroundtaskhost.exe","svchost.exe")
// Project interesting columns
| extend WdacPolicyName = parse_json(AdditionalFields)["PolicyName"]
| project TimeGenerated, DeviceName, ActionType, FileName, InitiatingProcessSHA1, InitiatingProcessFileName, 
    InitiatingProcessId, InitiatingProcessAccountName, InitiatingProcessParentFileName, WdacPolicyName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP,
    NonceRequestTimestamp, RdpTimeGenerated, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteMacAddress, RdpRemoteDeviceOnboardingStatus,
    RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported

Stages and Predicates

Parameters

let time_lookback = 30d;

Let Binding: admin_users

let admin_users = toscalar(
    IdentityInfo
    | where Timestamp > ago(7d)
    | where CriticalityLevel != "" or AccountDisplayName contains "Admin"
    | summarize make_set(AccountDisplayName)
);

Let Binding: devices_with_admin_accounts used in Stages 1, 8

let devices_with_admin_accounts = (
    ExposureGraphEdges
    | where SourceNodeLabel == "device" and TargetNodeLabel == "user"
    | where TargetNodeName in (admin_users)
    | make-graph SourceNodeId --> TargetNodeId with ExposureGraphNodes on NodeId
    | graph-match (SourceNode)-[hasCredentialsOf]->(TargetNode)
        project IncomingNodeName = SourceNode.NodeName, OutgoingNodeName = TargetNode.NodeName, CriticalityLevel = TargetNode.NodeProperties.rawData.criticalityLevel.criticalityLevel, CriticalityRuleNames = TargetNode.NodeProperties.rawData.criticalityLevel.ruleNames
    | summarize make_set(IncomingNodeName)
);

Let Binding: no_tpm_devices used in Stage 2

let no_tpm_devices = (
    ExposureGraphNodes
    | where NodeLabel == "device"
    | mv-expand EntityIds
    | where EntityIds.type == "DeviceInventoryId"
    | extend OnboardingStatus = tostring(parse_json(NodeProperties)["rawData"]["onboardingStatus"]),
        TpmSupported = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["supported"]),
        TpmEnabled = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["enabled"]),
        TpmActivated = tostring(parse_json(NodeProperties)["rawData"]["tpmData"]["activated"]),
        DeviceName = tostring(parse_json(NodeProperties)["rawData"]["deviceName"]),
        DeviceId = tostring(EntityIds.id)
    | distinct DeviceId, DeviceName, OnboardingStatus, TpmSupported, TpmEnabled, TpmActivated
    | where OnboardingStatus != "Onboarded" or (TpmSupported != "true" and TpmActivated != "true" and TpmEnabled != "true")
    | extend TpmSupported = iff(TpmSupported == "", "unknown", TpmSupported),
        TpmActivated = iff(TpmActivated == "", "unknown", TpmActivated),
        TpmEnabled = iff(TpmEnabled == "", "unknown", TpmEnabled)
);

Let Binding: no_tpm_device_info used in Stage 3

let no_tpm_device_info = (
    DeviceNetworkInfo
    | where Timestamp > ago(7d)
    | summarize arg_max(Timestamp, *) by DeviceId
    | mv-expand todynamic(IPAddresses)
    | extend IPAddress = tostring(IPAddresses.IPAddress)
    | join kind=inner no_tpm_devices on DeviceId
    | project DeviceId, DeviceName, MacAddress, IPAddress, OnboardingStatus, TpmActivated, TpmEnabled, TpmSupported
);

Let Binding: dangerous_rdp_sessions used in Stages 4, 12

let dangerous_rdp_sessions = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where DeviceName in (devices_with_admin_accounts)
    | where InitiatingProcessFileName !~ "microsoft.tri.sensor.exe"
    | where ActionType == "ConnectionSuccess"
    | where RemotePort == 3389
    | join kind=inner no_tpm_device_info on $left.RemoteIP == $right.IPAddress
    | project-rename RemoteDeviceId = DeviceId1, 
        RdpRemoteDeviceName = DeviceName1, 
        RdpRemoteMacAddress = MacAddress, 
        RdpRemoteDeviceOnboardingStatus = OnboardingStatus, 
        RdpRemoteDeviceTpmActivated = TpmActivated, 
        RdpRemoteDeviceTpmEnabled = TpmEnabled, 
        RdpRemoteDeviceTpmSupported = TpmSupported,
        RdpTimeGenerated = Timestamp,
        RdpInitiatingProcessFileName = InitiatingProcessFileName
    | project-away IPAddress
);

Let Binding: nonce_requests used in Stages 5, 10

let nonce_requests = (
    DeviceNetworkEvents
    | where Timestamp > ago(time_lookback)
    | where ActionType == "ConnectionSuccess"
    | where RemoteUrl =~ "login.microsoftonline.com"
    | project-rename NonceRequestTimestamp = Timestamp
);

Stage 1: source

let devices_with_admin_accounts

Stage 2: source

let no_tpm_devices

Stage 3: source

let no_tpm_device_info

Stage 4: source

let dangerous_rdp_sessions

Stage 5: source

let nonce_requests

Stage 6: source

DeviceEvents

Stage 7: where

where Timestamp > ago(2592000s)

Stage 8: where

where DeviceName == "devices_with_admin_accounts"

Stage 9: where

where ActionType startswith "AppControl" and FileName =~ "ncrypt.dll"

Stage 10: join

join kind=inner (nonce_requests) on InitiatingProcessId, DeviceId

Stage 11: where

where /* macro: (Timestamp between (todatetime((NonceRequestTimestamp - 10m)) .. todatetime((NonceRequestTimestamp + 10m)))) */

Stage 12: join

join kind=inner (dangerous_rdp_sessions) on DeviceId

Stage 13: where

where not (InitiatingProcessFileName in ("backgroundtaskhost.exe", "svchost.exe"))

Stage 14: extend

extend WdacPolicyName

Stage 15: project

project ActionType, DeviceName, FileName, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessId, InitiatingProcessParentFileName, InitiatingProcessRemoteSessionDeviceName, InitiatingProcessRemoteSessionIP, InitiatingProcessSHA1, NonceRequestTimestamp, RdpInitiatingProcessFileName, RdpRemoteDeviceName, RdpRemoteDeviceOnboardingStatus, RdpRemoteDeviceTpmActivated, RdpRemoteDeviceTpmEnabled, RdpRemoteDeviceTpmSupported, RdpRemoteMacAddress, RdpTimeGenerated, Timestamp, WdacPolicyName

Stage 16: summarize aggregation inside the join branch

summarize by DeviceId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize

F&O - Mass update or deletion of user records

#
Status
available
Severity
medium
Time window
1h
Group by
LogType, TableName, Username
Source
github.com/Azure/Azure-Sentinel

Identifies large delete or update operations on Finance & Operations user records based on predefined thresholds.

MITRE ATT&CK Coverage

Rule Body

id: 5ab00fbb-ba2c-44dc-b02e-f119639b9a11
kind: Scheduled
name: F&O - Mass update or deletion of user records
description: Identifies large delete or update operations on Finance & Operations
  user records based on predefined thresholds.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dynamics365Finance
    dataTypes:
      - FinanceOperationsActivity_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1485
  - T1565
  - T1491
query: |
  // Set threshold for number of updated or deleted records
  let update_detection_threshold = 50;
  let deleted_detection_threshold = 10;
  FinanceOperationsActivity_CL
  | where TableName == "UserInfo" and LogType in ("Update", "Delete")
  | summarize
      TotalEvents = count(),
      StartTime = min(LogCreatedDateTime),
      EndTime = max(LogCreatedDateTime)
      by TableName, Username, LogType
  | where (LogType == "Update" and TotalEvents > update_detection_threshold) or (LogType == "Delete" and TotalEvents > deleted_detection_threshold)
  | extend FinOpsAppId = 32780
  | project StartTime, EndTime, Username, LogType, TableName, TotalEvents, FinOpsAppId
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Username
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: FinOpsAppId
alertDetailsOverride:
  alertDisplayNameFormat: F&O - many user account records deleted
  alertDescriptionFormat: '{{TotalEvents}} user records deleted in F&O by user {{Username}}'
version: 3.2.0

Stages and Predicates

Parameters

let update_detection_threshold = 50;
let deleted_detection_threshold = 10;

Stage 1: source

FinanceOperationsActivity_CL

Stage 2: where

| where TableName == "UserInfo" and LogType in ("Update", "Delete")

Stage 3: summarize

| summarize
    TotalEvents = count(),
    StartTime = min(LogCreatedDateTime),
    EndTime = max(LogCreatedDateTime)
    by TableName, Username, LogType

Stage 4: where

| where (LogType == "Update" and TotalEvents > update_detection_threshold) or (LogType == "Delete" and TotalEvents > deleted_detection_threshold)

Stage 5: extend

| extend FinOpsAppId = 32780

Stage 6: project

| project StartTime, EndTime, Username, LogType, TableName, TotalEvents, FinOpsAppId

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LogTypeeq
  • Delete
  • Update
field:"LogType" kind:eq
LogTypein
  • Delete
  • Update
field:"LogType" kind:in
TableNameeq
  • UserInfo
field:"TableName" kind:eq value:"UserInfo"
TotalEventsgt
  • 10 transforms: number
  • 50 transforms: number
field:"TotalEvents" kind:gt

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimeproject
FinOpsAppIdproject
LogTypeproject
StartTimeproject
TableNameproject
TotalEventsproject
Usernameproject

Google SecOps - Single-Event Alert

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

Creates incidents in Microsoft Sentinel when Google Security Operations raises an active single-event alert (SINGLE_EVENT, riskScore gte 40) at MEDIUM, HIGH, or CRITICAL severity. These alerts represent a single action like malware execution, credential abuse, or defense evasion severe enough to trigger an incident without requiring multi-signal correlation.

MITRE ATT&CK Coverage

Rule Body

id: e5a9b143-7e6c-4f0a-c4b1-9d3f5a8e1c7f
name: Google SecOps - Single-Event Alert
description: |
  Creates incidents in Microsoft Sentinel when Google Security Operations raises an active single-event alert (SINGLE_EVENT, riskScore gte 40) at MEDIUM, HIGH, or CRITICAL severity. These alerts represent a single action like malware execution, credential abuse, or defense evasion severe enough to trigger an incident without requiring multi-signal correlation.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GSDetectionAlerts
    dataTypes:
      - DetectionAlerts_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - CredentialAccess
  - DefenseEvasion
  - Impact
relevantTechniques:
  - T1059
  - T1110
  - T1562
  - T1485
query: |
  GoogleSecOpsDetectionAlerts
  | where ruleType == "SINGLE_EVENT"
  | where severity in ("HIGH", "CRITICAL", "MEDIUM")
  | where riskScore >= 40
  | where alertState == "ALERTING"
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: varPrincipalIp
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: varTargetIp
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: varSourceIp
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: varCorrelationIp
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: urlBackToProduct
customDetails:
  RuleName: ruleName
  RuleType: ruleType
  Severity: severity
  RiskScore: riskScore
  DetectionTime: detectionTime
  PrincipalIP: varPrincipalIp
  TargetIP: varTargetIp
  SourceIP: varSourceIp
  CorrelationIP: varCorrelationIp
  PrincipalHostname: varPrincipalHostname
  TargetHostname: varTargetHostname
  SourceHostname: varSourceHostname
  PrincipalUser: varPrincipalUserUserid
  SourceUser: varSourceUserUserid
  TargetUser: varTargetUserUserid
  alert_identifier: id
alertDetailsOverride:
  alertDisplayNameFormat: "Single-Event : {{ruleName}} : {{id}}"
  alertDescriptionFormat: "Google SecOps flagged a single high-severity event as an active alert requiring immediate action. Rule: {{ruleName}}."
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByCustomDetails:
      - alert_identifier
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

GoogleSecOpsDetectionAlerts

Stage 2: where

| where ruleType == "SINGLE_EVENT"

Stage 3: where

| where severity in ("HIGH", "CRITICAL", "MEDIUM")

Stage 4: where

| where riskScore >= 40

Stage 5: where

| where alertState == "ALERTING"

Indicators

These rows show field, operator, and value matches.

GSA - TI IP Entity

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

This query identifies IP indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in GSA NetworkAccessTraffic.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 3ebd25b1-6f54-49f9-b5a5-0246357ce4ca
name: GSA - TI IP Entity
description: |
  This query identifies IP indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in GSA NetworkAccessTraffic.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - NetworkAccessTrafficLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let NetworkAccessTraffic_ =
      NetworkAccessTraffic
      | where TimeGenerated >= ago(dt_lookBack)
      | where TrafficType == "internet"
      | project-rename GSANetworkAccessTraffic_TimeGenerated = TimeGenerated;
  let IPList = NetworkAccessTraffic_ | summarize by DestinationIp;
  ThreatIntelIndicators
  | project-rename TI_ipEntity = ObservableValue
  | extend IndicatorType = replace(@'\[|\]|"""', "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType in ("ipv4-addr", "network-traffic")
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | where TimeGenerated >= ago(ioc_lookBack)
  | where TI_ipEntity in (IPList)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, TI_ipEntity
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  | join kind=innerunique (NetworkAccessTraffic_) on $left.TI_ipEntity == $right.DestinationIp
  | extend GSAThreatType = ThreatType
  | summarize GSANetworkAccessTraffic_TimeGenerated = arg_max(GSANetworkAccessTraffic_TimeGenerated, *) by Id, DestinationIp
  | project-reorder *,
      Tags,
      Confidence,
      TrafficLightProtocolLevel,
      Type,
      TI_ipEntity,
      UserPrincipalName,
      UserId,
      DeviceId,
      InitiatingProcessName,
      GSAThreatType,
      DestinationFqdn,
      DestinationUrl,
      DestinationPort,
      SourceIp,
      SourcePort,
      HttpUserAgent
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserPrincipalName
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: Host
    fieldMappings:
      - identifier: AzureID
        columnName: DeviceId
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: NetworkAccessTraffic_ used in Stage 12

let NetworkAccessTraffic_ = NetworkAccessTraffic
    | where TimeGenerated >= ago(dt_lookBack)
    | where TrafficType == "internet"
    | project-rename GSANetworkAccessTraffic_TimeGenerated = TimeGenerated;

Let Binding: IPList used in Stage 7

let IPList = NetworkAccessTraffic_ | summarize by DestinationIp;

Stage 1: source

ThreatIntelIndicators

Stage 2: project-rename

| project-rename TI_ipEntity = ObservableValue

Stage 3: extend

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

Stage 4: where

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

Stage 5: extend

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

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: where

| where TI_ipEntity in (IPList)

Stage 8: summarize

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

Stage 9: where

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

Stage 10: extend

| extend Description = tostring(parse_json(Data).description)

Stage 11: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 12: join

| join kind=innerunique (NetworkAccessTraffic_) on $left.TI_ipEntity == $right.DestinationIp

Stage 13: extend

extend GSAThreatType

Stage 14: summarize

summarize GSANetworkAccessTraffic_TimeGenerated by Id, DestinationIp

Stage 15: project-reorder

project-reorder

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationIpsummarize
GSANetworkAccessTraffic_TimeGeneratedsummarize
Idsummarize

Lumen TI IPAddress in WindowsEvents

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

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

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 4776281c-6c49-46ac-8444-4dd8ba2f4565
name: Lumen TI IPAddress in WindowsEvents
displayName: Lumen TI IPAddress in WindowsEvents
description: |
  This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in WindowsEvents.
severity: Medium
requiredDataConnectors:
  - connectorId: LumenThreatFeedConnector
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceUploadIndicatorsAPI
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvents
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1d;  // Data lookback for WindowsEvents
  let ioc_lookBack = 14d; // TI lookback
  let IP_Indicators = ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | where IsActive == true and ValidUntil > now()
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
    | where SourceSystem == 'Lumen'
    | where ObservableKey == 'ipv4-addr:value'
    | extend TI_ipEntity = ObservableValue
    | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith 'fe80' and TI_ipEntity !startswith '::' and TI_ipEntity !startswith '127.';
  IP_Indicators
  | join kind=innerunique (
      WindowsEvent
      | where TimeGenerated >= ago(dt_lookBack)
      | extend WE_ipEntity = tostring(EventData.IpAddress)
      | extend WindowsEvent_TimeGenerated = TimeGenerated
    ) on $left.TI_ipEntity == $right.WE_ipEntity
  | where WindowsEvent_TimeGenerated < ValidUntil
  | summarize arg_max(WindowsEvent_TimeGenerated, *), StartTime = min(WindowsEvent_TimeGenerated), EndTime = max(WindowsEvent_TimeGenerated) by Id, WE_ipEntity
  | project timestamp = EndTime, StartTime, EndTime, Computer, EventID, Channel, Id, Tags, ValidUntil, Confidence, TI_ipEntity, WE_ipEntity, Type
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: WE_ipEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: where

| where IsActive == true and ValidUntil > now()

Stage 4: summarize

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

Stage 5: where

| where SourceSystem == 'Lumen'

Stage 6: where

| where ObservableKey == 'ipv4-addr:value'

Stage 7: extend

| extend TI_ipEntity = ObservableValue

Stage 8: where

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

Stage 9: join

IP_Indicators
| join kind=innerunique (
    WindowsEvent
    | where TimeGenerated >= ago(dt_lookBack)
    | extend WE_ipEntity = tostring(EventData.IpAddress)
    | extend WindowsEvent_TimeGenerated = TimeGenerated
  ) on $left.TI_ipEntity == $right.WE_ipEntity

Stage 10: where

where WindowsEvent_TimeGenerated < ValidUntil

Stage 11: summarize

summarize EndTime, StartTime by Id, WE_ipEntity

Stage 12: project

project Channel, Computer, Confidence, EndTime, EventID, Id, StartTime, TI_ipEntity, Tags, Type, ValidUntil, WE_ipEntity, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IsActiveeq
  • true corpus 12 (kusto 12)
field:"IsActive" kind:eq value:"true"
ObservableKeyeq
  • ipv4-addr:value corpus 10 (kusto 10)
field:"ObservableKey" kind:eq value:"ipv4-addr:value"
SourceSystemeq
  • Lumen corpus 8 (kusto 8)
field:"SourceSystem" kind:eq value:"Lumen"

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
Channelproject
Computerproject
Confidenceproject
EndTimeproject
EventIDproject
Idproject
StartTimeproject
TI_ipEntityproject
Tagsproject
Typeproject
ValidUntilproject
WE_ipEntityproject
timestampproject

Palo Alto Prisma Cloud - Inactive user

#
Status
available
Severity
low
Time window
1d
Source
github.com/Azure/Azure-Sentinel

'Detects users inactive for 30 days.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 7f78fa52-9833-41de-b5c5-76e61b8af9c1
name: Palo Alto Prisma Cloud - Inactive user
description: |
  'Detects users inactive for 30 days.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: PaloAltoPrismaCloud
    dataTypes:
      - PaloAltoPrismaCloud
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  PaloAltoPrismaCloud
  | where Status =~ 'open'
  | where AlertMessage has 'Inactive users for more than 30 days'
  | extend AccountCustomEntity = ResourceId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

PaloAltoPrismaCloud

Stage 2: where

| where Status =~ 'open'

Stage 3: where

| where AlertMessage has 'Inactive users for more than 30 days'

Stage 4: extend

| extend AccountCustomEntity = ResourceId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

SlackAudit - Multiple archived files uploaded in short period of time

#
Status
available
Severity
low
Time window
15m
Group by
SrcUserName, bucket
Source
github.com/Azure/Azure-Sentinel

'This query helps to detect when a user uploads multiple archived files in short period of time.'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 3db0cb83-5fa4-4310-a8a0-d8d66183f0bd
name: SlackAudit - Multiple archived files uploaded in short period of time
description: |
  'This query helps to detect when a user uploads multiple archived files in short period of time.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: SlackAuditAPI
    dataTypes:
      - SlackAudit_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics: 
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  let threshold = 10;
  SlackAudit
  | where DvcAction =~ 'file_uploaded'
  | extend FE = tolower(tostring(split(EntityFileName, '.')[-1]))
  | where FE in~ ('tar', 'bz2', 'gz', 'tgz', 'z', 'tbz2', 'zst', 'zip', 'zipx', '7z', 'rar', 'sfx')
  | summarize UploadCount = count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SrcUserName, bucket = bin(TimeGenerated, 15m)
  | where UploadCount > threshold
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

SlackAudit

Stage 2: where

| where DvcAction =~ 'file_uploaded'

Stage 3: extend

| extend FE = tolower(tostring(split(EntityFileName, '.')[-1]))

Stage 4: where

| where FE in~ ('tar', 'bz2', 'gz', 'tgz', 'z', 'tbz2', 'zst', 'zip', 'zipx', '7z', 'rar', 'sfx')

Stage 5: summarize

| summarize UploadCount = count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SrcUserName, bucket = bin(TimeGenerated, 15m)

Stage 6: where

| where UploadCount > threshold

Stage 7: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DvcActioneq
  • file_uploaded
field:"DvcAction" kind:eq value:"file_uploaded"
FEin
  • 7z
  • bz2
  • gz
  • rar
  • sfx
  • tar
  • tbz2
  • tgz
  • z
  • zip
  • zipx
  • zst
field:"FE" kind:in
UploadCountgt
  • 10 transforms: number
field:"UploadCount" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
SrcUserNamesummarize
StartTimesummarize
UploadCountsummarize
bucketsummarize
AccountCustomEntityextend

Tailscale Premium: Posture integration disabled or removed

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

Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance - removal weakens fleet posture and is a possible defense-evasion step.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Defense Impairment

Rule Body

id: a1b2c3d4-5678-9012-34ab-cdef12345030
name: "Tailscale Premium: Posture integration disabled or removed"
description: Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance - removal weakens fleet posture and is a possible defense-evasion step.
description-detailed: |
  Identifies when a device-posture integration is disabled or removed from the tailnet. Posture integrations enforce device compliance; removing one disables that enforcement and increases blast radius for compromised endpoints. Requires Tailscale Premium or Enterprise.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Persistence
relevantTechniques:
  - T1562
  - T1556
query: |
  Tailscale_Audit_CL
  | where Action in ("DELETE", "UPDATE")
  | where tostring(Target.type) == "POSTURE_INTEGRATION"
    or tostring(Target.type) startswith "POSTURE"
  | extend ActorLogin = tostring(Actor.loginName)
  | extend Provider = tostring(Target.name)
  | project TimeGenerated, ActorLogin, Action, Provider, Target, Old, New, Origin
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action in ("DELETE", "UPDATE")

Stage 3: where

| where tostring(Target.type) == "POSTURE_INTEGRATION"
  or tostring(Target.type) startswith "POSTURE"

Stage 4: extend

| extend ActorLogin = tostring(Actor.loginName)

Stage 5: extend

| extend Provider = tostring(Target.name)

Stage 6: project

| project TimeGenerated, ActorLogin, Action, Provider, Target, Old, New, Origin

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionin
  • DELETE
  • UPDATE
field:"Action" kind:in
Target.typeeq
  • POSTURE_INTEGRATION transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:eq value:"POSTURE_INTEGRATION"
Target.typestarts_with
  • POSTURE transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:starts_with value:"POSTURE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActorLoginproject
Newproject
Oldproject
Originproject
Providerproject
Targetproject
TimeGeneratedproject

Theom - Unencrypted public data stores

#

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 TRIS0005 (Theom has observed data stores that are both unencrypted and publicly accessible. Review if the data store and the data within should be publicly accessible. Additionally, encrypt the data at rest to comply with these CIS requirements)"

MITRE ATT&CK Coverage

Rule Body

id: 6b93d8b1-40cf-4973-adaa-6f240df21ff1
name: Theom - Unencrypted public data stores
description: |
   "Creates Sentinel incidents for critical/high Theom risks, associated with ruleId TRIS0005 (Theom has observed data stores that are both unencrypted and publicly accessible. Review if the data store and the data within should be publicly accessible. Additionally, encrypt the data at rest to comply with these CIS requirements)"
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 == "TRIS0005" 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 == "TRIS0005" and (priority_s == "P1" or priority_s == "P2")

Indicators

These rows show field, operator, and value matches.

TI Map Domain entity to PaloAlto CommonSecurityLog

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

Identifies a match in PaloAlto CommonSecurityLog table from any Domain IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 094a4e6e-1a0d-4d49-9d64-cfc3b01a0be1
name: TI Map Domain entity to PaloAlto CommonSecurityLog
description: |
  Identifies a match in PaloAlto CommonSecurityLog table from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour
  let ioc_lookBack = 14d; // Look back 14 days
  // Create a list of top-level domains (TLDs) from the threat feed data for later validation
  let SecurityLog = materialize(
    CommonSecurityLog
      // Filter common security logs based on the specified time range
      | extend IngestionTime = ingestion_time()
      | where IngestionTime > ago(dt_lookBack)
      | where DeviceEventClassID =~ 'url'
      // Uncomment the line below to only alert on allowed connections
      //| where DeviceAction !~ "block-url"
      // Extract the domain from RequestURL, if not present, extract it from AdditionalExtensions
      | extend PA_Url = column_ifexists("RequestURL", "None")
      | extend PA_Url = iif(isempty(PA_Url) and AdditionalExtensions !startswith "PanOS", extract("([^\\\"]+)", 1, tolower(AdditionalExtensions)), trim('"', PA_Url))
      | extend PA_Url = iif(PA_Url !startswith "http://" and ApplicationProtocol !~ "ssl", strcat('http://', PA_Url), iif(PA_Url !startswith "https://" and ApplicationProtocol =~ "ssl", strcat('https://', PA_Url), PA_Url))
      | extend Domain = trim('"', tostring(parse_url(PA_Url).Host))
      | where isnotempty(Domain)
      | extend Domain = tolower(Domain)
      | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  );
  let LogDomains = SecurityLog | distinct Domain | summarize make_list(Domain);
  // Retrieve threat intelligence indicators within the specified time range
  let Domain_Indicators = materialize(
      ThreatIntelIndicators
      | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
      | where IndicatorType == "domain-name"
      | extend DomainName = tolower(ObservableValue)
      | where TimeGenerated >= ago(ioc_lookBack)
      | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
      | where IsActive and (ValidUntil > now() or isempty(ValidUntil)));
  // Join threat intelligence indicators with common security logs
  Domain_Indicators | join kind=innerunique (SecurityLog) on $left.DomainName == $right.Domain
  | where CommonSecurityLog_TimeGenerated < ValidUntil
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id
  | extend Description = tostring(parse_json(Data).description), ActivityGroupNames = tostring(parse_json(Data).activitygroupnames)
  | project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, PA_Url, Domain, Id, ValidUntil, Confidence, DeviceAction, DestinationIP, DestinationPort, DeviceName, SourceIP, SourcePort, ApplicationProtocol, RequestMethod, Type
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: PA_Url
version: 1.4.6
kind: Scheduled

Stages and Predicates

Parameters

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

let Domain_Indicators is inlined into the numbered stages below.

Let Binding: SecurityLog used in Stage 8

let SecurityLog = materialize(
  CommonSecurityLog
    | extend IngestionTime = ingestion_time()
    | where IngestionTime > ago(dt_lookBack)
    | where DeviceEventClassID =~ 'url'
    | extend PA_Url = column_ifexists("RequestURL", "None")
    | extend PA_Url = iif(isempty(PA_Url) and AdditionalExtensions !startswith "PanOS", extract("([^\\\"]+)", 1, tolower(AdditionalExtensions)), trim('"', PA_Url))
    | extend PA_Url = iif(PA_Url !startswith "http://" and ApplicationProtocol !~ "ssl", strcat('http://', PA_Url), iif(PA_Url !startswith "https://" and ApplicationProtocol =~ "ssl", strcat('https://', PA_Url), PA_Url))
    | extend Domain = trim('"', tostring(parse_url(PA_Url).Host))
    | where isnotempty(Domain)
    | extend Domain = tolower(Domain)
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
);

Let Binding: LogDomains

let LogDomains = SecurityLog | distinct Domain | summarize make_list(Domain);

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

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType == "domain-name"

Stage 4: extend

| extend DomainName = tolower(ObservableValue)

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: summarize

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

Stage 7: where

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

Stage 8: join

Domain_Indicators
| join kind=innerunique (SecurityLog) on $left.DomainName == $right.Domain

Stage 9: where

where CommonSecurityLog_TimeGenerated < ValidUntil

Stage 10: summarize

summarize CommonSecurityLog_TimeGenerated by Id

Stage 11: extend

extend ActivityGroupNames, Description

Stage 12: project

project ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, Confidence, Description, DestinationIP, DestinationPort, DeviceAction, DeviceName, Domain, Id, PA_Url, RequestMethod, SourceIP, SourcePort, Type, ValidUntil

Stage 13: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ApplicationProtocolproject
CommonSecurityLog_TimeGeneratedproject
Confidenceproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DeviceActionproject
DeviceNameproject
Domainproject
Idproject
PA_Urlproject
RequestMethodproject
SourceIPproject
SourcePortproject
Typeproject
ValidUntilproject
timestampextend

TI map Domain entity to SecurityAlert

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

'Identifies a match in SecurityAlert table from any Domain IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 87890d78-3e05-43ec-9ab9-ba32f4e01250
name: TI map Domain entity to SecurityAlert
description: |
  'Identifies a match in SecurityAlert table from any Domain IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert
  - 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 SecurityAlerts = SecurityAlert
  | where TimeGenerated > ago(dt_lookBack)
  | where AlertName != "TI map Domain entity to SecurityAlert"
  | extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
  | where isnotempty(domain)
  | mv-expand domain
  | extend domain = tostring(domain)
  | extend EntitiesDynamicArray = parse_json(Entities)
  | mv-apply EntitiesDynamicArray on
      (summarize
          HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
          IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
      )
  | extend Alert_TimeGenerated = TimeGenerated
  | extend Alert_Description = Description;
  let AlertDomains = SecurityAlerts
  | distinct domain
  | summarize make_list(domain);
  let Domain_Indicators = materialize(ThreatIntelligenceIndicator
  | where isnotempty(DomainName)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend TI_DomainEntity = tolower(DomainName)
  | where TI_DomainEntity in (AlertDomains)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;");
  Domain_Indicators
  // Using innerunique to keep performance fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (SecurityAlerts) on $left.TI_DomainEntity == $right.domain
  | where Alert_TimeGenerated < ExpirationDateTime
  | summarize Alert_TimeGenerated = arg_max(Alert_TimeGenerated, *) by IndicatorId, AlertName
  | project Alert_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, AlertName, Alert_Description, ProviderName, AlertSeverity, ConfidenceLevel, HostName, IP_addr, Url, Entities, Type, TI_DomainEntity
  | extend timestamp = Alert_TimeGenerated
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IP_addr
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.4
kind: Scheduled

Stages and Predicates

Parameters

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

let Domain_Indicators is inlined into the numbered stages below.

Let Binding: SecurityAlerts used in Stage 9

let SecurityAlerts = SecurityAlert
| where TimeGenerated > ago(dt_lookBack)
| where AlertName != "TI map Domain entity to SecurityAlert"
| extend domain = todynamic(dynamic_to_json(extract_all(@"(((xn--)?[a-z0-9\-]+\.)+([a-z]+|(xn--[a-z0-9]+)))", dynamic([1]), tolower(Entities))))
| where isnotempty(domain)
| mv-expand domain
| extend domain = tostring(domain)
| extend EntitiesDynamicArray = parse_json(Entities)
| mv-apply EntitiesDynamicArray on
    (summarize
        HostName = take_anyif(tostring(EntitiesDynamicArray.HostName), EntitiesDynamicArray.Type == "host"),
        IP_addr = take_anyif(tostring(EntitiesDynamicArray.Address), EntitiesDynamicArray.Type == "ip")
    )
| extend Alert_TimeGenerated = TimeGenerated
| extend Alert_Description = Description;

Let Binding: AlertDomains used in Stage 5

let AlertDomains = SecurityAlerts
| distinct domain
| summarize make_list(domain);

Stages 1 to 8 define let Domain_Indicators as the rule's main pipeline source. Stages 9 to 13 use it.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(DomainName)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend

| extend TI_DomainEntity = tolower(DomainName)

Stage 5: where

| where TI_DomainEntity in (AlertDomains)

Stage 6: summarize

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

Stage 7: where

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

Stage 8: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 9: join

Domain_Indicators
| join kind=innerunique (SecurityAlerts) on $left.TI_DomainEntity == $right.domain

Stage 10: where

where Alert_TimeGenerated < ExpirationDateTime

Stage 11: summarize

summarize Alert_TimeGenerated by IndicatorId, AlertName

Stage 12: project

project ActivityGroupNames, AlertName, AlertSeverity, Alert_Description, Alert_TimeGenerated, ConfidenceLevel, ConfidenceScore, Description, DomainName, Entities, ExpirationDateTime, HostName, IP_addr, IndicatorId, ProviderName, TI_DomainEntity, ThreatType, Type, Url

Stage 13: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AlertNameproject
AlertSeverityproject
Alert_Descriptionproject
Alert_TimeGeneratedproject
ConfidenceLevelproject
ConfidenceScoreproject
Descriptionproject
DomainNameproject
Entitiesproject
ExpirationDateTimeproject
HostNameproject
IP_addrproject
IndicatorIdproject
ProviderNameproject
TI_DomainEntityproject
ThreatTypeproject
Typeproject
Urlproject
timestampextend

TI map File Hash to CommonSecurityLog Event

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

'Identifies a match in CommonSecurityLog Event data from any FileHash IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 5d33fc63-b83b-4913-b95e-94d13f0d379f
name: TI map File Hash to CommonSecurityLog Event
description: |
  'Identifies a match in CommonSecurityLog Event data from any FileHash IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - 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 fileHashIndicators = ThreatIntelligenceIndicator
  | where isnotempty(FileHashValue)
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now();
  // Handle matches against both lower case and uppercase versions of the hash:
  (fileHashIndicators | extend  FileHashValue = tolower(FileHashValue)
  | union (fileHashIndicators | extend FileHashValue = toupper(FileHashValue)))
  // 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 (
    CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack)
    | where isnotempty(FileHash)
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
    )
  on $left.FileHashValue == $right.FileHash
  | where CommonSecurityLog_TimeGenerated < ExpirationDateTime
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by IndicatorId, FileHashValue
  | project CommonSecurityLog_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  SourceIP, SourcePort, DestinationIP, DestinationPort, SourceUserID, SourceUserName, DeviceName, DeviceAction,
  RequestURL, DestinationUserName, DestinationUserID, ApplicationProtocol, Activity, FileHashValue, FileHashType
  | extend HostName = tostring(split(DeviceName, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(DeviceName, '.'), 1, -1), '.'))
  | extend Name = tostring(split(SourceUserName, '@', 0)[0]), UPNSuffix = tostring(split(SourceUserName, '@', 1)[0])
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SourceUserName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: FileHash
    fieldMappings:
      - identifier: Value
        columnName: FileHashValue
      - identifier: Algorithm
        columnName: FileHashType
version: 1.3.6
kind: Scheduled

Stages and Predicates

Parameters

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

let fileHashIndicators is inlined into the numbered stages below.

Stages 1 to 5 define let fileHashIndicators as the rule's main pipeline source. Stages 6 to 18 combine it with further branches.

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(FileHashValue)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

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

Stage 5: where

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

Stage 6: extend

fileHashIndicators
| extend  FileHashValue = tolower(FileHashValue)

Stage 7: union

union of 1 branches

Stage 8: source

ThreatIntelligenceIndicator

Stage 9: where

| where isnotempty(FileHashValue)

Stage 10: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 11: summarize

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

Stage 12: where

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

Stage 13: extend

| extend FileHashValue = toupper(FileHashValue)

Stage 14: join

| join kind=innerunique (
  CommonSecurityLog | where TimeGenerated >= ago(dt_lookBack)
  | where isnotempty(FileHash)
  | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  )
on $left.FileHashValue == $right.FileHash

Stage 15: where

where CommonSecurityLog_TimeGenerated < ExpirationDateTime

Stage 16: summarize

summarize CommonSecurityLog_TimeGenerated by IndicatorId, FileHashValue

Stage 17: project

project Activity, ActivityGroupNames, ApplicationProtocol, CommonSecurityLog_TimeGenerated, ConfidenceScore, Description, DestinationIP, DestinationPort, DestinationUserID, DestinationUserName, DeviceAction, DeviceName, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, RequestURL, SourceIP, SourcePort, SourceUserID, SourceUserName, ThreatType, Url

Stage 18: extend (3 consecutive steps)

extend DnsDomain, HostName, Name, UPNSuffix, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true
field:"Active" kind:eq value:"true"
FileHashis_not_null
  • (no value, null check)
field:"FileHash" kind:is_not_null
FileHashValueis_not_null
  • (no value, null check)
field:"FileHashValue" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
ActivityGroupNamesproject
ApplicationProtocolproject
CommonSecurityLog_TimeGeneratedproject
ConfidenceScoreproject
Descriptionproject
DestinationIPproject
DestinationPortproject
DestinationUserIDproject
DestinationUserNameproject
DeviceActionproject
DeviceNameproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
RequestURLproject
SourceIPproject
SourcePortproject
SourceUserIDproject
SourceUserNameproject
ThreatTypeproject
Urlproject
DnsDomainextend
HostNameextend
Nameextend
UPNSuffixextend
timestampextend

Trend Micro CAS - DLP violation

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

'Detects when DLP policy violation occurs.'

MITRE ATT&CK Coverage

Rule Body

id: 1ddeb8ad-cad9-4db4-b074-f9da003ca3ed
name: Trend Micro CAS - DLP violation
description: |
  'Detects when DLP policy violation occurs.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  TrendMicroCAS
  | where isnotempty(TriggeredDlpTemplate)
  | extend AccountCustomEntity = DstUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

TrendMicroCAS

Stage 2: where

| where isnotempty(TriggeredDlpTemplate)

Stage 3: extend

| extend AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
TriggeredDlpTemplateis_not_null
  • (no value, null check)
field:"TriggeredDlpTemplate" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Back to all Kusto rules