Detection rules › Kusto
Kusto rules: 0m
AWSCloudTrail - Privilege escalation via Glue policy
#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
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry Coverage
Rules Detecting the Same Action
These rules filter on the same operation.
- AWS IAM Access Key Compromise Detection (Panther)
- AWS Lateral Movement from Kubernetes SA via AssumeRoleWithWebIdentity (Elastic)
- AWSCloudTrail - Privilege escalation via CloudFormation policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD DynamoDB policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD IAM policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD KMS policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD Lambda policy (Kusto)
- AWSCloudTrail - Privilege escalation via CRUD S3 policy (Kusto)
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | contains |
| field:"Action" kind:contains |
Effect | eq |
| field:"Effect" kind:eq value:"Allow" |
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | in |
| field:"aws::eventName" kind:in |
Resource | eq |
| field:"Resource" kind:eq value:"*" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
PolicyName | extend |
Statement | extend |
Action | extend |
Condition | extend |
Effect | extend |
Resource | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |
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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| 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.
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"T0ADCSESC6b" |
Cisco ASA - Possible Data Exfiltration Detection
#'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
| Tactic | Techniques |
|---|---|
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationIP | is_not_null | field:"DestinationIP" kind:is_not_null | |
DeviceAction | in |
| field:"DeviceAction" kind:in |
DeviceProduct | match |
| field:"DeviceProduct" kind:match value:"ASA" |
SentBytesLong | is_not_null | field:"SentBytesLong" kind:is_not_null | |
SourceIP | cidr_match |
| field:"SourceIP" kind:cidr_match |
SourceIP | is_not_null | field:"SourceIP" kind:is_not_null | |
TotalBytesSent | ge |
| field:"TotalBytesSent" kind:ge value:"1073741824" |
UploadRatio | ge |
| field:"UploadRatio" kind:ge value:"95.0" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DestinationIP | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:DestinationIP |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Applications | project |
Connections | project |
DestinationIP | project |
DestinationPorts | project |
FirstSeen | project |
LastSeen | project |
SourceIP | project |
TimeGenerated | project |
TotalGBReceived | project |
TotalGBSent | project |
UploadRatio | project |
Cloudflare - Unexpected client request
#'Detects client requests to unusual client request.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
ClientRequestURI | match |
| field:"ClientRequestURI" kind:match |
DstBytes | ne |
| field:"DstBytes" kind:ne value:"0" |
HttpRequestMethod | eq |
| field:"HttpRequestMethod" kind:eq value:"GET" |
SrcBytes | ne |
| field:"SrcBytes" kind:ne value:"0" |
Detect Suspicious ncrypt.dll usage on admin device with RDP connections to non TPM protected device
#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
| Tactic | Techniques |
|---|---|
| Credential Access | |
| Lateral Movement |
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
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
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
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
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
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.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
InitiatingProcessFileName | in | backgroundtaskhost.exe, svchost.exe | excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"backgroundtaskhost.exe" field:"InitiatingProcessFileName" value:"svchost.exe" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DeviceId | summarize |
F&O - Mass update or deletion of user records
#Identifies large delete or update operations on Finance & Operations user records based on predefined thresholds.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Impact |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
LogType | eq |
| field:"LogType" kind:eq |
LogType | in |
| field:"LogType" kind:in |
TableName | eq |
| field:"TableName" kind:eq value:"UserInfo" |
TotalEvents | gt |
| field:"TotalEvents" kind:gt |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EndTime | project |
FinOpsAppId | project |
LogType | project |
StartTime | project |
TableName | project |
TotalEvents | project |
Username | project |
Google SecOps - Single-Event Alert
#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
| Tactic | Techniques |
|---|---|
| Execution | |
| Defense Impairment | |
| Credential Access | |
| Impact |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
alertState | eq |
| field:"alertState" kind:eq value:"ALERTING" |
riskScore | ge |
| field:"riskScore" kind:ge value:"40" |
ruleType | eq |
| field:"ruleType" kind:eq value:"SINGLE_EVENT" |
severity | in |
| field:"severity" kind:in |
GSA - TI IP Entity
#This query identifies IP indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in GSA NetworkAccessTraffic.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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_
let NetworkAccessTraffic_ = NetworkAccessTraffic
| where TimeGenerated >= ago(dt_lookBack)
| where TrafficType == "internet"
| project-rename GSANetworkAccessTraffic_TimeGenerated = TimeGenerated;
Let Binding: IPList
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.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | in |
| field:"IndicatorType" kind:in |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
TI_ipEntity | in |
| field:"ObservableValue" kind:in value:"IPList" |
TrafficType | eq |
| field:"TrafficType" kind:eq value:"internet" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationIp | summarize |
GSANetworkAccessTraffic_TimeGenerated | summarize |
Id | summarize |
Lumen TI IPAddress in WindowsEvents
#This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in WindowsEvents.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ObservableKey | eq |
| field:"ObservableKey" kind:eq value:"ipv4-addr:value" |
SourceSystem | eq |
| field:"SourceSystem" kind:eq value:"Lumen" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
TI_ipEntity | cidr_match | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8 | excludes:TI_ipEntity |
TI_ipEntity | starts_with | 127. | excludes:TI_ipEntity field:"TI_ipEntity" value:"127." |
TI_ipEntity | starts_with | :: | excludes:TI_ipEntity field:"TI_ipEntity" value:"::" |
TI_ipEntity | starts_with | fe80 | excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Channel | project |
Computer | project |
Confidence | project |
EndTime | project |
EventID | project |
Id | project |
StartTime | project |
TI_ipEntity | project |
Tags | project |
Type | project |
ValidUntil | project |
WE_ipEntity | project |
timestamp | project |
Palo Alto Prisma Cloud - Inactive user
#'Detects users inactive for 30 days.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
AlertMessage | match |
| field:"AlertMessage" kind:match value:"Inactive users for more than 30 days" |
Status | eq |
| field:"Status" kind:eq value:"open" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
SlackAudit - Multiple archived files uploaded in short period of time
#'This query helps to detect when a user uploads multiple archived files in short period of time.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
DvcAction | eq |
| field:"DvcAction" kind:eq value:"file_uploaded" |
FE | in |
| field:"FE" kind:in |
UploadCount | gt |
| field:"UploadCount" kind:gt value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
EndTime | summarize |
SrcUserName | summarize |
StartTime | summarize |
UploadCount | summarize |
bucket | summarize |
AccountCustomEntity | extend |
Tailscale Premium: Posture integration disabled or removed
#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
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Action | in |
| field:"Action" kind:in |
Target.type | eq |
| field:"Target.type" kind:eq value:"POSTURE_INTEGRATION" |
Target.type | starts_with |
| field:"Target.type" kind:starts_with value:"POSTURE" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Action | project |
ActorLogin | project |
New | project |
Old | project |
Origin | project |
Provider | project |
Target | project |
TimeGenerated | project |
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.
"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
| Tactic | Techniques |
|---|---|
| Collection |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
customProps_RuleId_s | eq |
| field:"customProps_RuleId_s" kind:eq value:"TRIS0005" |
priority_s | eq |
| field:"priority_s" kind:eq |
TI Map Domain entity to PaloAlto CommonSecurityLog
#Identifies a match in PaloAlto CommonSecurityLog table from any Domain IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
DeviceEventClassID | eq |
| field:"DeviceEventClassID" kind:eq value:"url" |
Domain | is_not_null | field:"Domain" kind:is_not_null | |
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
ValidUntil | is_null | field:"ValidUntil" kind:is_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ApplicationProtocol | project |
CommonSecurityLog_TimeGenerated | project |
Confidence | project |
Description | project |
DestinationIP | project |
DestinationPort | project |
DeviceAction | project |
DeviceName | project |
Domain | project |
Id | project |
PA_Url | project |
RequestMethod | project |
SourceIP | project |
SourcePort | project |
Type | project |
ValidUntil | project |
timestamp | extend |
TI map Domain entity to SecurityAlert
#'Identifies a match in SecurityAlert table from any Domain IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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
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
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.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
AlertName | ne |
| field:"AlertName" kind:ne value:"TI map Domain entity to SecurityAlert" |
DomainName | is_not_null | field:"DomainName" kind:is_not_null | |
TI_DomainEntity | in |
| field:"TI_DomainEntity" kind:in value:"AlertDomains" |
domain | is_not_null | field:"domain" kind:is_not_null |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
Description | contains | State: falsepos; | excludes:Description field:"Description" value:"State: falsepos;" |
Description | contains | State: inactive; | excludes:Description field:"Description" value:"State: inactive;" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
AlertName | project |
AlertSeverity | project |
Alert_Description | project |
Alert_TimeGenerated | project |
ConfidenceLevel | project |
ConfidenceScore | project |
Description | project |
DomainName | project |
Entities | project |
ExpirationDateTime | project |
HostName | project |
IP_addr | project |
IndicatorId | project |
ProviderName | project |
TI_DomainEntity | project |
ThreatType | project |
Type | project |
Url | project |
timestamp | extend |
TI map File Hash to CommonSecurityLog Event
#'Identifies a match in CommonSecurityLog Event data from any FileHash IOC from TI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| 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.
| Field | Kind | Values | Search |
|---|---|---|---|
Active | eq |
| field:"Active" kind:eq value:"true" |
FileHash | is_not_null | field:"FileHash" kind:is_not_null | |
FileHashValue | is_not_null | field:"FileHashValue" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
ActivityGroupNames | project |
ApplicationProtocol | project |
CommonSecurityLog_TimeGenerated | project |
ConfidenceScore | project |
Description | project |
DestinationIP | project |
DestinationPort | project |
DestinationUserID | project |
DestinationUserName | project |
DeviceAction | project |
DeviceName | project |
ExpirationDateTime | project |
FileHashType | project |
FileHashValue | project |
IndicatorId | project |
RequestURL | project |
SourceIP | project |
SourcePort | project |
SourceUserID | project |
SourceUserName | project |
ThreatType | project |
Url | project |
DnsDomain | extend |
HostName | extend |
Name | extend |
UPNSuffix | extend |
timestamp | extend |
Trend Micro CAS - DLP violation
#'Detects when DLP policy violation occurs.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration |
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.
| Field | Kind | Values | Search |
|---|---|---|---|
TriggeredDlpTemplate | is_not_null | field:"TriggeredDlpTemplate" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |