Detection rules › Kusto
Kusto rules: 0b
1Password - User account MFA settings changed
#This will alert when a user creates, updates, or disables the user account MFA settings. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| 1Password | any: User (catch-all) |
Rule Body
id: 3c8140eb-e946-4bf2-8c61-03e4df56d400
name: 1Password - User account MFA settings changed
version: 1.0.0
kind: Scheduled
description: |-
This will alert when a user creates, updates, or disables the user account MFA settings. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.
Ref: https://1password.com/
Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
- connectorId: 1Password
dataTypes:
- OnePasswordEventLogs_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- DefenseEvasion
relevantTechniques:
- T1556
query: |-
OnePasswordEventLogs_CL
| where log_source == "auditevents"
| where action has_any("enblmfa", "updatmfa", "disblmfa")
| where object_type == "user"
| extend
ActorUsername = actor_details.email
, SrcIpAddr = session.ip
suppressionEnabled: false
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: 1h
matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ActorUsername
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SrcIpAddr
Stages and Predicates
Stage 1: source
OnePasswordEventLogs_CL
Stage 2: where
| where log_source == "auditevents"
Stage 3: where
| where action has_any("enblmfa", "updatmfa", "disblmfa")
Stage 4: where
| where object_type == "user"
Stage 5: extend
| extend
ActorUsername = actor_details.email
, SrcIpAddr = session.ip
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
action | match |
| field:"action" kind:match |
log_source | eq |
| field:"log_source" kind:eq value:"auditevents" |
object_type | eq |
| field:"object_type" kind:eq value:"user" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActorUsername | extend |
SrcIpAddr | extend |
Alsid DCSync
#'Searches for DCSync attacks'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: d3c658bd-8da9-4372-82e4-aaffa922f428
name: Alsid DCSync
description: |
'Searches for DCSync attacks'
severity: High
requiredDataConnectors:
- connectorId: AlsidForAD
dataTypes:
- AlsidForADLog_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- CredentialAccess
relevantTechniques:
- T1003.006
query: |
afad_parser
| where MessageType == 2 and Codename == "DCSync"
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
version: 1.0.1
kind: Scheduled
Stages and Predicates
Stage 1: source
afad_parser
Stage 2: where
| where MessageType == 2 and Codename == "DCSync"
Stage 3: extend
| extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Codename | eq |
| field:"Codename" kind:eq value:"DCSync" |
MessageType | eq |
| field:"MessageType" kind:eq value:"2" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DnsDomain | extend |
HostName | extend |
ApexOne - Device access permissions was changed
#'Query shows device access permissions was changed.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Rule Body
id: b463b952-67b8-11ec-90d6-0242ac120003
name: ApexOne - Device access permissions was changed
description: |
'Query shows device access permissions was changed.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |
let threshold = 1;
TMApexOneEvent
| where EventMessage has "Device access"
| extend DeviceCustomNumber3 = coalesce(
column_ifexists("FieldDeviceCustomNumber3", long(null)),
DeviceCustomNumber3,
long(null)
)
| extend Permission = case(
DeviceCustomNumber3 == "0", "Modify",
DeviceCustomNumber3 == "1", "Read and execute",
DeviceCustomNumber3 == "2", "Read",
DeviceCustomNumber3 == "3", "List device content only",
DeviceCustomNumber3 == "4", "Block",
"unknown"
)
| summarize Permissions = make_set(Permission) by DstUserName
| extend PermissionCount = array_length(Permissions)
| where PermissionCount > threshold
| extend AccountCustomEntity = DstUserName
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.4
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 1;
Stage 1: source
TMApexOneEvent
Stage 2: where
| where EventMessage has "Device access"
Stage 3: extend
| extend DeviceCustomNumber3 = coalesce(
column_ifexists("FieldDeviceCustomNumber3", long(null)),
DeviceCustomNumber3,
long(null)
)
Stage 4: extend
| extend Permission = case(
DeviceCustomNumber3 == "0", "Modify",
DeviceCustomNumber3 == "1", "Read and execute",
DeviceCustomNumber3 == "2", "Read",
DeviceCustomNumber3 == "3", "List device content only",
DeviceCustomNumber3 == "4", "Block",
"unknown"
)
Permission =DeviceCustomNumber3 == 0"Modify"DeviceCustomNumber3 == 1"Read and execute"DeviceCustomNumber3 == 2"Read"DeviceCustomNumber3 == 3"List device content only"DeviceCustomNumber3 == 4"Block""unknown"Stage 5: summarize
| summarize Permissions = make_set(Permission) by DstUserName
Stage 6: extend
| extend PermissionCount = array_length(Permissions)
Stage 7: where
| where PermissionCount > threshold
Stage 8: extend
| extend AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"Device access" |
PermissionCount | gt |
| field:"PermissionCount" kind:gt value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DstUserName | summarize |
Permissions | summarize |
PermissionCount | extend |
AccountCustomEntity | extend |
ApexOne - Inbound remote access connection
#'Detects inbound remote access connection.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Lateral Movement |
Rule Body
id: 6303235a-ee70-42a4-b969-43e7b969b916
name: ApexOne - Inbound remote access connection
description: |
'Detects inbound remote access connection.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CefAma
dataTypes:
- CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
relevantTechniques:
- T1021
query: |
TMApexOneEvent
| where EventSubType has "WB"
| where NetworkDirection == 1
| where ApplicationProtocol in ('15', '17', '20', '22', '23', '27', '36', '37')
| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Stage 1: source
TMApexOneEvent
Stage 2: where
| where EventSubType has "WB"
Stage 3: where
| where NetworkDirection == 1
Stage 4: where
| where ApplicationProtocol in ('15', '17', '20', '22', '23', '27', '36', '37')
Stage 5: extend
| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ApplicationProtocol | in |
| field:"ApplicationProtocol" kind:in |
EventSubType | match |
| field:"EventSubType" kind:match value:"WB" |
NetworkDirection | eq |
| field:"NetworkDirection" kind:eq value:"1" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountCustomEntity | extend |
IPCustomEntity | extend |
Attempt to Update Security Object Failed
#Detects failed attempts to update security objects in Veeam Backup & Replication. Security objects include users and roles, credential records, certificates, or passwords.
Rule Body
id: 54c9a609-60db-47b0-82ee-86895c89bd89
name: Attempt to Update Security Object Failed
description: Detects failed attempts to update security objects in Veeam Backup &
Replication. Security objects include users and roles, credential records, certificates,
or passwords.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
dataTypes:
- Syslog
- connectorId: SyslogAma
dataTypes:
- Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 41810\n| extend Endpoint =\
\ extract(\"param2=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n Date\
\ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n DataSource = original_host,\n\
\ EventId = instanceId,\n [\"User Name\"] = user,\n [\"Endpoint\"] = Endpoint,\n\
\ MessageDetails = Description,\n Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
Date: Date
VbrHostName: DataSource
EventId: EventId
MessageDetails: MessageDetails
Severity: Severity
Stages and Predicates
Stage 1: source
Veeam_GetSecurityEvents
Stage 2: where
| where instanceId == 41810
Stage 3: extend
| extend Endpoint = extract("param2=\"([^\"]*)\"", 1, SyslogMessage)
Stage 4: project
| project
Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
DataSource = original_host,
EventId = instanceId,
["User Name"] = user,
["Endpoint"] = Endpoint,
MessageDetails = Description,
Severity = SeverityDescription
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
instanceId | eq |
| field:"instanceId" kind:eq value:"41810" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DataSource | project |
Date | project |
Endpoint | project |
EventId | project |
MessageDetails | project |
Severity | project |
User Name | project |
BloodHound Attack Path Finding - Large Default Groups in PS Remote Users Groups
#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 in PS Remote Users Groups
Rule Body
id: ac15bea7-c2c2-4f0f-b13c-2099f4d86f77
name: BloodHound Attack Path Finding - Large Default Groups in PS Remote Users Groups
description: BloodHound Attack Path Finding - Large Default Groups in PS Remote Users
Groups
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 == "LargeDefaultGroupsPSRemote"
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 == "LargeDefaultGroupsPSRemote"
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Finding | eq |
| field:"Finding" kind:eq value:"LargeDefaultGroupsPSRemote" |
Cross-tenant Access Settings Organization Inbound Collaboration Settings Changed
#Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Inbound Collaboration Settings are changed for "Users & Groups" and for "Applications".
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Discovery |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules Detecting the Same Action
These rules filter on the same operation.
- Cross-tenant Access Settings Organization Added (Kusto)
- Cross-tenant Access Settings Organization Deleted (Kusto)
- Cross-tenant Access Settings Organization Inbound Direct Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Outbound Collaboration Settings Changed (Kusto)
- Cross-tenant Access Settings Organization Outbound Direct Settings Changed (Kusto)
Rule Body
id: c895c5b9-0fc6-40ce-9830-e8818862f2d5
name: Cross-tenant Access Settings Organization Inbound Collaboration Settings Changed
description: |
'Organizations are added in the Cross-tenant Access Settings to control communication inbound or outbound for users and applications. This detection notifies when Organization Inbound Collaboration Settings are changed for "Users & Groups" and for "Applications".'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 2d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Persistence
- Discovery
relevantTechniques:
- T1078.004
- T1136.003
- T1087.004
query: |
// In User & Groups and in Applications, the following "AccessType" values in columns PremodifiedInboundSettings and ModifiedInboundSettings are interpreted accordingly
// When Access Type in premodified inbound settings value was 1 that means that the initial access was allowed. When Access Type in premodified inbound settings value was 2 that means that the initial access was blocked.
// When Access Type in modified inbound settings value is 1 that means that now access is allowed. When Access Type in modified inbound settings value is 2 that means that now access is blocked.
AuditLogs
| where OperationName has "Update a partner cross-tenant access setting"
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
| mv-apply Property = Properties on
(
where Property.displayName =~ "b2bCollaborationInbound"
| extend PremodifiedInboundSettings = trim('"',tostring(Property.oldValue)),
ModifiedInboundSettings = trim(@'"',tostring(Property.newValue))
)
| where PremodifiedInboundSettings != ModifiedInboundSettings
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatingAppName
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled
Stages and Predicates
Stage 1: source
AuditLogs
Stage 2: where
| where OperationName has "Update a partner cross-tenant access setting"
Stage 3: kusto:mv-apply
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type =~ "Policy"
| extend Properties = TargetResource.modifiedProperties
)
Stage 4: kusto:mv-apply
| mv-apply Property = Properties on
(
where Property.displayName =~ "b2bCollaborationInbound"
| extend PremodifiedInboundSettings = trim('"',tostring(Property.oldValue)),
ModifiedInboundSettings = trim(@'"',tostring(Property.newValue))
)
Stage 5: where
| where PremodifiedInboundSettings != ModifiedInboundSettings
Stage 6: extend (6 consecutive steps)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | match |
| field:"OperationName" kind:match value:"Update a partner cross-tenant access setting" |
Property.displayName | eq |
| field:"Property.displayName" kind:eq value:"b2bCollaborationInbound" |
TargetResource.type | eq |
| field:"TargetResource.type" kind:eq value:"Policy" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InitiatingAppName | extend |
InitiatingAppServicePrincipalId | extend |
InitiatingUserPrincipalName | extend |
InitiatingAadUserId | extend |
InitiatingIpAddress | extend |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |
CYFIRMA - Attack Surface - Configuration Medium Rule
#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.
This alert is generated when CYFIRMA detects a critical misconfiguration in a public-facing asset or service. Such misconfigurations may include exposed admin interfaces, default credentials, open directory listings, or insecure protocols, which significantly increase the attack surface."
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Reconnaissance | |
| Initial Access | |
| Execution | |
| Persistence | |
| Stealth | |
| Credential Access | |
| Discovery | |
| Collection |
Rule Body
id: e1f88d08-5c32-4d35-a8ce-2f21cdb4b6de
name: CYFIRMA - Attack Surface - Configuration Medium Rule
description: |
This alert is generated when CYFIRMA detects a critical misconfiguration in a public-facing asset or service.
Such misconfigurations may include exposed admin interfaces, default credentials, open directory listings, or insecure protocols, which significantly increase the attack surface."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
- connectorId: CyfirmaAttackSurfaceAlertsConnector
dataTypes:
- CyfirmaASConfigurationAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
- InitialAccess
- Discovery
- Persistence
- Execution
- DefenseEvasion
- CredentialAccess
- Collection
- Reconnaissance
relevantTechniques:
- T1190
- T1087
- T1046
- T1136
- T1059
- T1566
- T1070
- T1027
- T1505
- T1555
- T1114
- T1595
query: |
// Medium Severity - Attack Surface - Misconfiguration Detected
let timeFrame = 5m;
CyfirmaASConfigurationAlerts_CL
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=sub_domain,
TopDomain=top_domain,
NetworkIP=ip,
AlertUID=alert_uid,
UID=uid,
Softwares=software,
WebAppFirewall=web_app_firewall,
ClickJackingDefence=click_jacking_defence,
ContentSecurityPolicy=content_security_policy,
CookieXssProtection=cookie_xss_protection,
DataInjectionDefence=data_injection_defence,
DomainStatus=domain_status,
MissingEPPCodes=missing_epp_codes,
SecureCookie=secure_cookie,
SetCookieHttpsOnly=set_cookie_https_only,
XFrameOptions=x_frame_options,
X_XssProtection=x_xss_protection,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
| project
TimeGenerated,
Description,
Domain,
TopDomain,
RiskScore,
FirstSeen,
LastSeen,
NetworkIP,
AlertUID,
UID,
Softwares,
WebAppFirewall,
ClickJackingDefence,
ContentSecurityPolicy,
CookieXssProtection,
DataInjectionDefence,
DomainStatus,
MissingEPPCodes,
SecureCookie,
SetCookieHttpsOnly,
XFrameOptions,
X_XssProtection,
ProviderName,
ProductName
entityMappings:
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: Domain
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: TopDomain
- identifier: DnsDomain
columnName: Domain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: NetworkIP
customDetails:
TimeGenerated: TimeGenerated
RiskScore: RiskScore
FirstSeen: FirstSeen
LastSeen: LastSeen
AlertUID: AlertUID
UID: UID
Softwares: Softwares
WebAppFirewall: WebAppFirewall
ClickJackingDefence: ClickJackingDefence
SecurityPolicy: ContentSecurityPolicy
CookieXssProtection: CookieXssProtection
InjectionDefence: DataInjectionDefence
DomainStatus: DomainStatus
MissingEPPCodes: MissingEPPCodes
SecureCookie: SecureCookie
SetCookieHttpsOnly: SetCookieHttpsOnly
XFrameOptions: XFrameOptions
X_XssProtection: X_XssProtection
alertDetailsOverride:
alertDisplayNameFormat: "CYFIRMA - Medium Risk Misconfiguration Identified in Assets - Domain: {{Domain}} , IP: {{NetworkIP}}"
alertDescriptionFormat: "CYFIRMA - Medium Risk Misconfiguration Identified in Assets - {{Description}}"
alertDynamicProperties:
- alertProperty: ProductName
value: ProductName
- alertProperty: ProviderName
value: ProviderName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: false
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
eventGroupingSettings:
aggregationKind: AlertPerResult
Stages and Predicates
Parameters
let timeFrame = 5m;
Stage 1: source
CyfirmaASConfigurationAlerts_CL
Stage 2: where
| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
Stage 3: extend
| extend
Description=description,
FirstSeen=first_seen,
LastSeen=last_seen,
RiskScore=risk_score,
Domain=sub_domain,
TopDomain=top_domain,
NetworkIP=ip,
AlertUID=alert_uid,
UID=uid,
Softwares=software,
WebAppFirewall=web_app_firewall,
ClickJackingDefence=click_jacking_defence,
ContentSecurityPolicy=content_security_policy,
CookieXssProtection=cookie_xss_protection,
DataInjectionDefence=data_injection_defence,
DomainStatus=domain_status,
MissingEPPCodes=missing_epp_codes,
SecureCookie=secure_cookie,
SetCookieHttpsOnly=set_cookie_https_only,
XFrameOptions=x_frame_options,
X_XssProtection=x_xss_protection,
ProviderName='CYFIRMA',
ProductName='DeCYFIR/DeTCT'
Stage 4: project
| project
TimeGenerated,
Description,
Domain,
TopDomain,
RiskScore,
FirstSeen,
LastSeen,
NetworkIP,
AlertUID,
UID,
Softwares,
WebAppFirewall,
ClickJackingDefence,
ContentSecurityPolicy,
CookieXssProtection,
DataInjectionDefence,
DomainStatus,
MissingEPPCodes,
SecureCookie,
SetCookieHttpsOnly,
XFrameOptions,
X_XssProtection,
ProviderName,
ProductName
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
severity | eq |
| field:"severity" kind:eq value:"High" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlertUID | project |
ClickJackingDefence | project |
ContentSecurityPolicy | project |
CookieXssProtection | project |
DataInjectionDefence | project |
Description | project |
Domain | project |
DomainStatus | project |
FirstSeen | project |
LastSeen | project |
MissingEPPCodes | project |
NetworkIP | project |
ProductName | project |
ProviderName | project |
RiskScore | project |
SecureCookie | project |
SetCookieHttpsOnly | project |
Softwares | project |
TimeGenerated | project |
TopDomain | project |
UID | project |
WebAppFirewall | project |
XFrameOptions | project |
X_XssProtection | project |
Cynerio - IoT - Weak password
#User signed in using weak credentials
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Credential Access |
Rule Body
id: 65db1346-6435-4079-bbf4-9a7113c98054
name: Cynerio - IoT - Weak password
description: User signed in using weak credentials
severity: High
tags:
- Schema: ASimAuthentication
SchemaVersion: 0.1.3
requiredDataConnectors:
- connectorId: CynerioSecurityEvents
dataTypes:
- CynerioEvent_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
relevantTechniques:
- T1552
query: |
CynerioEvent_CL
| where TimeGenerated > ago(24h)
| where module_s == 'IDS'
| where title_s == 'Weak Password'
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: asset_ip_s
eventGroupingSettings:
aggregationKind: SingleAlert
customDetails:
Service: service_s
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
CynerioEvent_CL
Stage 2: where
| where TimeGenerated > ago(24h)
Stage 3: where
| where module_s == 'IDS'
Stage 4: where
| where title_s == 'Weak Password'
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
module_s | eq |
| field:"module_s" kind:eq value:"IDS" |
title_s | eq |
| field:"title_s" kind:eq value:"Weak Password" |
GCP Audit Logs - VPC Flow Logs Disabled
#Detects when Google Cloud Platform VPC Flow Logs configurations are disabled or deleted. VPC Flow Logs capture information about IP traffic going to and from network interfaces in VPC networks, providing critical visibility for security monitoring and forensic analysis. Disabling VPC Flow Logs reduces network visibility and may indicate an attempt to evade detection before performing malicious activities. Adversaries may disable flow logs to hide lateral movement, data exfiltration, or command and control traffic.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| GCP | google.cloud.networkmanagement.VpcFlowLogsService.DeleteVpcFlowLogsConfig: DeleteVpcFlowLogsConfig |
Rule Body
id: 8f3e9c2d-5b4a-4d6e-9a7c-2f8b5e1d3c9a
name: GCP Audit Logs - VPC Flow Logs Disabled
description: |
'Detects when Google Cloud Platform VPC Flow Logs configurations are disabled or deleted.
VPC Flow Logs capture information about IP traffic going to and from network interfaces in VPC networks, providing critical visibility for security monitoring and forensic analysis.
Disabling VPC Flow Logs reduces network visibility and may indicate an attempt to evade detection before performing malicious activities.
Adversaries may disable flow logs to hide lateral movement, data exfiltration, or command and control traffic.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: GCPAuditLogsDefinition
dataTypes:
- GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562.001
tags:
- GCP
- VPC Flow Logs
- Network Security
query: |
GCPAuditLogs
| where ServiceName == "networkmanagement.googleapis.com"
| where MethodName has_any ("VpcFlowLogsService.UpdateVpcFlowLogsConfig", "VpcFlowLogsService.DeleteVpcFlowLogsConfig")
| extend
RequestJson = parse_json(Request),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo),
AuthzInfoJson = parse_json(AuthorizationInfo)
| extend
FlowLogsConfigName = split(GCPResourceName, "/")[-1],
ConfigState = tostring(RequestJson.vpc_flow_logs_config.state),
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
AuthEmail = tostring(AuthInfoJson.principalEmail),
Permission = tostring(AuthzInfoJson[0].permission),
PermissionType = tostring(AuthzInfoJson[0].permissionType),
PermissionGranted = tostring(AuthzInfoJson[0].granted)
| where PermissionType == "ADMIN_WRITE"
| where MethodName has "DeleteVpcFlowLogsConfig" or ConfigState == "DISABLED"
| extend
Action = case(
MethodName has "DeleteVpcFlowLogsConfig", "Deleted",
ConfigState == "DISABLED", "Disabled",
"Modified"),
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
| project TimeGenerated,
PrincipalEmail,
AuthEmail,
ProjectId,
ResourceName = GCPResourceName,
FlowLogsConfigName,
Action,
CallerIpAddress,
MethodName,
ServiceName,
Severity,
Permission,
PermissionGranted,
LogName,
InsertId,
AccountName,
AccountUPNSuffix
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: PrincipalEmail
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ProjectId
- identifier: InstanceName
columnName: ResourceName
customDetails:
ProjectId: ProjectId
FlowLogsConfigName: FlowLogsConfigName
ResourceName: ResourceName
Action: Action
Permission: Permission
alertDetailsOverride:
alertDisplayNameFormat: "GCP VPC Flow Logs {{FlowLogsConfigName}} {{Action}} by {{PrincipalEmail}}"
alertDescriptionFormat: |-
GCP VPC Flow Logs configuration {{FlowLogsConfigName}} {{Action}} in project {{ProjectId}}.
This action reduces network traffic visibility and may indicate an attempt to evade detection.
Investigate immediately to determine if this action was authorized and assess the security implications.
Review recent network activity before the logs were disabled for signs of malicious behavior.
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GCPAuditLogs
Stage 2: where
| where ServiceName == "networkmanagement.googleapis.com"
Stage 3: where
| where MethodName has_any ("VpcFlowLogsService.UpdateVpcFlowLogsConfig", "VpcFlowLogsService.DeleteVpcFlowLogsConfig")
Stage 4: extend
| extend
RequestJson = parse_json(Request),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo),
AuthzInfoJson = parse_json(AuthorizationInfo)
Stage 5: extend
| extend
FlowLogsConfigName = split(GCPResourceName, "/")[-1],
ConfigState = tostring(RequestJson.vpc_flow_logs_config.state),
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
AuthEmail = tostring(AuthInfoJson.principalEmail),
Permission = tostring(AuthzInfoJson[0].permission),
PermissionType = tostring(AuthzInfoJson[0].permissionType),
PermissionGranted = tostring(AuthzInfoJson[0].granted)
Stage 6: where
| where PermissionType == "ADMIN_WRITE"
Stage 7: where
| where MethodName has "DeleteVpcFlowLogsConfig" or ConfigState == "DISABLED"
Stage 8: extend
| extend
Action = case(
MethodName has "DeleteVpcFlowLogsConfig", "Deleted",
ConfigState == "DISABLED", "Disabled",
"Modified"),
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
Action =MethodName has "DeleteVpcFlowLogsConfig""Deleted"ConfigState == "DISABLED""Disabled""Modified"Stage 9: project
| project TimeGenerated,
PrincipalEmail,
AuthEmail,
ProjectId,
ResourceName = GCPResourceName,
FlowLogsConfigName,
Action,
CallerIpAddress,
MethodName,
ServiceName,
Severity,
Permission,
PermissionGranted,
LogName,
InsertId,
AccountName,
AccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ConfigState | eq |
| field:"ConfigState" kind:eq value:"DISABLED" |
MethodName | match |
| field:"gcp::method_name" kind:match |
PermissionType | eq |
| field:"PermissionType" kind:eq value:"ADMIN_WRITE" |
ServiceName | eq |
| field:"ServiceName" kind:eq value:"networkmanagement.googleapis.com" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AccountName | project |
AccountUPNSuffix | project |
Action | project |
AuthEmail | project |
CallerIpAddress | project |
FlowLogsConfigName | project |
InsertId | project |
LogName | project |
MethodName | project |
Permission | project |
PermissionGranted | project |
PrincipalEmail | project |
ProjectId | project |
ResourceName | project |
ServiceName | project |
Severity | project |
TimeGenerated | project |
GWorkspace - Unexpected OS update
#'Detects unexpected OS update.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Stealth |
Rule Body
id: c02b0c8e-5da6-11ec-bf63-0242ac130002
name: GWorkspace - Unexpected OS update
description: |
'Detects unexpected OS update.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: GoogleWorkspaceReportsAPI
dataTypes:
- GWorkspaceActivityReports
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Persistence
relevantTechniques:
- T1036
- T1554
query: |
GWorkspaceActivityReports
| where EventMessage has "os_updated"
| where isnotempty(NeqValue) and isnotempty(OldValue)
| extend NewVersion1 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 1, NeqValue)
| extend NewVersion2 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 2, NeqValue)
| extend NewVersion3 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 3, NeqValue)
| extend OldVersion1 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 1, OldValue)
| extend OldVersion2 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 2, OldValue)
| extend OldVersion3 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 3, OldValue)
| extend NewVersion3Comparision = iff(isempty(NewVersion3), 0, tolong(NewVersion3))
| extend OldVersion3Comparision = iff(isempty(OldVersion3), 0, tolong(OldVersion3))
| extend Comparision1 = iff(tolong(NewVersion3Comparision) > tolong(OldVersion3Comparision), 'Expected version', 'Unexpected version')
| extend Comparision2 = iff(tolong(NewVersion2) > tolong(OldVersion2), 'Expected version', Comparision1)
| extend VersionCheck = iff(tolong(NewVersion1) > tolong(OldVersion1), 'Expected version', Comparision2)
| project-away NewVersion1, NewVersion2, NewVersion3, NewVersion3Comparision, OldVersion1, OldVersion2, OldVersion3, OldVersion3Comparision, Comparision1, Comparision2
| extend AccountCustomEntity = ActorEmail
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled
Stages and Predicates
Stage 1: source
GWorkspaceActivityReports
Stage 2: where
| where EventMessage has "os_updated"
Stage 3: where
| where isnotempty(NeqValue) and isnotempty(OldValue)
Stage 4: extend (11 consecutive steps)
| extend NewVersion1 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 1, NeqValue)
| extend NewVersion2 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 2, NeqValue)
| extend NewVersion3 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 3, NeqValue)
| extend OldVersion1 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 1, OldValue)
| extend OldVersion2 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 2, OldValue)
| extend OldVersion3 = extract(@'([0-9]+)\.([0-9]+)\.?([0-9])?', 3, OldValue)
| extend NewVersion3Comparision = iff(isempty(NewVersion3), 0, tolong(NewVersion3))
| extend OldVersion3Comparision = iff(isempty(OldVersion3), 0, tolong(OldVersion3))
| extend Comparision1 = iff(tolong(NewVersion3Comparision) > tolong(OldVersion3Comparision), 'Expected version', 'Unexpected version')
| extend Comparision2 = iff(tolong(NewVersion2) > tolong(OldVersion2), 'Expected version', Comparision1)
| extend VersionCheck = iff(tolong(NewVersion1) > tolong(OldVersion1), 'Expected version', Comparision2)
Stage 5: project-away
| project-away NewVersion1, NewVersion2, NewVersion3, NewVersion3Comparision, OldVersion1, OldVersion2, OldVersion3, OldVersion3Comparision, Comparision1, Comparision2
Stage 6: extend
| extend AccountCustomEntity = ActorEmail
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
EventMessage | match |
| field:"EventMessage" kind:match value:"os_updated" |
NeqValue | is_not_null | field:"NeqValue" kind:is_not_null | |
OldValue | is_not_null | field:"OldValue" kind:is_not_null |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
VersionCheck | extend |
AccountCustomEntity | extend |
Hunt for public facing devices and exposed ports over time
#Find public facing devices over time via the public device tag in the DeviceInfo table.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
References
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Microsoft Defender for Endpoint | DeviceInfo action any: Device information |
Rule Body
// Create a base function
let base = (){
DeviceInfo
| where Timestamp > ago(30d)
| extend AdditionalFields = todynamic(AdditionalFields)
| extend InternetFacingLastSeen = todatetime(AdditionalFields.InternetFacingLastSeen)
, InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
, InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
, InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
, InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
, InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
, InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)
};
base()
// Get the latest resport
| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
// Join with the earliest report
| join kind=inner ( base()
| summarize arg_min(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
) on DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
// Make a data point for each day between earliest and latest report
| extend Range = range(bin(InternetFacingLastSeen1, 1d), bin(InternetFacingLastSeen, 1d), 1d)
// Now expand all datapoints for dates the ports have been active
| mv-expand Range
| where Range != ""
| summarize count() by InternetFacingLocalPort, bin(todatetime(Range), 1d)
| render linechart
Stages and Predicates
let base is inlined into the numbered stages below.
Stage 0: let
let base = DeviceInfo <inlined as stages below>;
Stages 1 to 4 define let base as the rule's main pipeline source. Stages 5 to 11 use it.
Stage 1: source
DeviceInfo
Stage 2: where
| where Timestamp > ago(30d)
Stage 3: extend
| extend AdditionalFields = todynamic(AdditionalFields)
Stage 4: extend
| extend InternetFacingLastSeen = todatetime(AdditionalFields.InternetFacingLastSeen)
, InternetFacingReason = tostring(AdditionalFields.InternetFacingReason)
, InternetFacingLocalIp = tostring(AdditionalFields.InternetFacingLocalIp)
, InternetFacingPublicScannedIp = tostring(AdditionalFields.InternetFacingPublicScannedIp)
, InternetFacingLocalPort = tostring(AdditionalFields.InternetFacingLocalPort)
, InternetFacingPublicScannedPort = tostring(AdditionalFields.InternetFacingPublicScannedPort)
, InternetFacingTransportProtocol = tostring(AdditionalFields.InternetFacingTransportProtocol)
Stage 5: summarize
base
| summarize arg_max(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
Stage 6: join
| join kind=inner ( base()
| summarize arg_min(InternetFacingLastSeen, *) by DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
) on DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
Stage 7: extend
extend Range
Stage 8: mv-expand
mv-expand Range
Stage 9: where
where Range != ""
Stage 10: summarize
summarize by InternetFacingLocalPort
Stage 11: render
render
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
InternetFacingLocalPort | summarize |
Imperva - Multiple user agents from same source
#''Detects suspicious number of user agents from the same IP address.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 4e8032eb-f04d-4a30-85d3-b74bf2c8f204
name: Imperva - Multiple user agents from same source
description: |
''Detects suspicious number of user agents from the same IP address.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: ImpervaWAFCloudAPI
dataTypes:
- ImpervaWAFCloud
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let threshold = 10;
ImpervaWAFCloud
| summarize d_uas = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
| where d_uas >= threshold
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Parameters
let threshold = 10;
Stage 1: source
ImpervaWAFCloud
Stage 2: summarize
| summarize d_uas = dcount(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
Stage 3: where
| where d_uas >= threshold
Stage 4: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
d_uas | ge |
| field:"d_uas" kind:ge value:"10" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
SrcIpAddr | summarize |
d_uas | summarize |
IPCustomEntity | extend |
Imperva - Possible command injection
#'Detects requests with commands in URI.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: 6214f187-5840-4cf7-a174-0cf9a72bfd29
name: Imperva - Possible command injection
description: |
'Detects requests with commands in URI.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: ImpervaWAFCloudAPI
dataTypes:
- ImpervaWAFCloud
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let cmd_lst = dynamic(['%2fetc%2fpasswd', '%2fetc%2fshadow', 'ping', 'whoami', 'phpinfo', '%2fbin%2fbash', 'curl', 'exec(', 'wget', 'python', 'gcc' , 'uname', 'systeminfo', 'rout', 'hostname', 'ifconfig']);
ImpervaWAFCloud
| where QueryString has_any (cmd_lst)
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.1
kind: Scheduled
Stages and Predicates
Let Binding: cmd_lst
let cmd_lst = dynamic(['%2fetc%2fpasswd', '%2fetc%2fshadow', 'ping', 'whoami', 'phpinfo', '%2fbin%2fbash', 'curl', 'exec(', 'wget', 'python', 'gcc' , 'uname', 'systeminfo', 'rout', 'hostname', 'ifconfig']);
Stage 1: source
ImpervaWAFCloud
Stage 2: where
| where QueryString has_any (cmd_lst)
Stage 3: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
QueryString | match |
| field:"QueryString" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
Lumen TI IPAddress in SigninLogs
#This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Entra ID | SigninLogs sign-in event: Sign-in record (SigninLogs, any result) |
Rule Body
id: 1425aea5-a9e5-4288-886e-934b90664a91
name: Lumen TI IPAddress in SigninLogs
displayName: Lumen TI IPAddress in SigninLogs
description: |
This query maps Lumen IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in SigninLogs.
severity: Medium
requiredDataConnectors:
- connectorId: LumenThreatFeedConnector
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: ThreatIntelligenceUploadIndicatorsAPI
dataTypes:
- ThreatIntelligenceIndicator
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
queryFrequency: 4h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 5h
suppressionEnabled: true
tactics:
- CommandAndControl
relevantTechniques:
- T1071
query: |
let dt_lookBack = 1d; // Data lookback for SigninLogs
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 (
SigninLogs
| where TimeGenerated >= ago(dt_lookBack)
| extend SL_ipEntity = IPAddress
| extend SigninLogs_TimeGenerated = TimeGenerated
) on $left.TI_ipEntity == $right.SL_ipEntity
| where SigninLogs_TimeGenerated < ValidUntil
| summarize arg_max(SigninLogs_TimeGenerated, *), StartTime = min(SigninLogs_TimeGenerated), EndTime = max(SigninLogs_TimeGenerated) by Id, SL_ipEntity
| project timestamp = EndTime, StartTime, EndTime, UserPrincipalName, IPAddress, ResultType, AppDisplayName, Id, Tags, ValidUntil, Confidence, TI_ipEntity, SL_ipEntity, Type
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SL_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 (
SigninLogs
| where TimeGenerated >= ago(dt_lookBack)
| extend SL_ipEntity = IPAddress
| extend SigninLogs_TimeGenerated = TimeGenerated
) on $left.TI_ipEntity == $right.SL_ipEntity
Stage 10: where
where SigninLogs_TimeGenerated < ValidUntil
Stage 11: summarize
summarize EndTime, StartTime by Id, SL_ipEntity
Stage 12: project
project AppDisplayName, Confidence, EndTime, IPAddress, Id, ResultType, SL_ipEntity, StartTime, TI_ipEntity, Tags, Type, UserPrincipalName, ValidUntil, 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 |
|---|---|
AppDisplayName | project |
Confidence | project |
EndTime | project |
IPAddress | project |
Id | project |
ResultType | project |
SL_ipEntity | project |
StartTime | project |
TI_ipEntity | project |
Tags | project |
Type | project |
UserPrincipalName | project |
ValidUntil | project |
timestamp | project |
NGINX - Command in URI
#'Detects command in URI'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: d84739ce-2f46-4391-b25e-a2edbea19d7e
name: NGINX - Command in URI
description: |
'Detects command in URI'
severity: High
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- NGINX_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
- T1133
query: |
let cmd_list = dynamic(['cat%20/etc/passwd', '/etc/passwd', 'ping -i', '/usr/bin/id(', '%2f%75%73%72%2f%62%69%6e%2f%69%64', 'phpinfo()', '%70%68%70%69%6e%66%6f%28%29', ';id', '%3b%69%64', '/bin/bash -c', '%2f%62%69%6e%2f%62%61%73%68%20%2d%63%27', '/bin/bash', '%2f%62%69%6e%2f%62%61%73%68', 'sleep(', '%73%6c%65%65%70%28', 'curl', '%63%75%72%6c', '&dir', '%26%64%69%72', '& dir', '%26%20%64%69%72', '<script>', '%3c%73%63%72%69%70%74%3e', 'eval(', '%65%76%61%6c%28', 'exec(', '%65%78%65%63%28', 'whoami', '%77%68%6f%61%6d%69', 'wget', 'python', 'gcc', 'uname', 'systeminfo', '%77%67%65%74', '%70%79%74%68%6f%6e', '%75%6e%61%6d%65', '%73%79%73%74%65%6d%69%6e%66%6f']);
NGINXHTTPServer
| where UrlOriginal has_any (cmd_list)
| extend UrlCustomEntity = UrlOriginal
entityMappings:
- entityType: URL
fieldMappings:
- identifier: Url
columnName: UrlCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Let Binding: cmd_list
let cmd_list = dynamic(['cat%20/etc/passwd', '/etc/passwd', 'ping -i', '/usr/bin/id(', '%2f%75%73%72%2f%62%69%6e%2f%69%64', 'phpinfo()', '%70%68%70%69%6e%66%6f%28%29', ';id', '%3b%69%64', '/bin/bash -c', '%2f%62%69%6e%2f%62%61%73%68%20%2d%63%27', '/bin/bash', '%2f%62%69%6e%2f%62%61%73%68', 'sleep(', '%73%6c%65%65%70%28', 'curl', '%63%75%72%6c', '&dir', '%26%64%69%72', '& dir', '%26%20%64%69%72', '<script>', '%3c%73%63%72%69%70%74%3e', 'eval(', '%65%76%61%6c%28', 'exec(', '%65%78%65%63%28', 'whoami', '%77%68%6f%61%6d%69', 'wget', 'python', 'gcc', 'uname', 'systeminfo', '%77%67%65%74', '%70%79%74%68%6f%6e', '%75%6e%61%6d%65', '%73%79%73%74%65%6d%69%6e%66%6f']);
Stage 1: source
NGINXHTTPServer
Stage 2: where
| where UrlOriginal has_any (cmd_list)
Stage 3: extend
| extend UrlCustomEntity = UrlOriginal
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
UrlOriginal | match |
| field:"UrlOriginal" kind:match |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
UrlCustomEntity | extend |
OCI - Inbound SSH connection
#'Detects inbound SSH connection.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Initial Access |
Rule Body
id: eb6e07a1-2895-4c55-9c27-ac84294f0e46
name: OCI - Inbound SSH connection
description: |
'Detects inbound SSH connection.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: OracleCloudInfrastructureLogsConnector
dataTypes:
- OCILogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1190
query: |
OCILogs
| where EventType contains 'vcn.flowlogs'
| where data_action_s =~ 'ACCEPT'
| where ipv4_is_private(DstIpAddr)
| where ipv4_is_private(SrcIpAddr) == False
| where DstPortNumber == 22
| extend IPCustomEntity = DstIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
OCILogs
Stage 2: where
| where EventType contains 'vcn.flowlogs'
Stage 3: where
| where data_action_s =~ 'ACCEPT'
Stage 4: where
| where ipv4_is_private(DstIpAddr)
Stage 5: where
| where ipv4_is_private(SrcIpAddr) == False
Stage 6: where
| where DstPortNumber == 22
Stage 7: extend
| extend IPCustomEntity = DstIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstIpAddr | cidr_match |
| field:"dest_ip" kind:cidr_match |
DstPortNumber | eq |
| field:"DstPortNumber" kind:eq value:"22" |
EventType | contains |
| field:"EventType" kind:contains value:"vcn.flowlogs" |
data_action_s | eq |
| field:"data_action_s" kind:eq value:"ACCEPT" |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
SrcIpAddr | 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:SrcIpAddr |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
SOCRadar Alarm Volume Spike
#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.
'Detects unusual spikes in SOCRadar alarm volume that may indicate an active campaign, coordinated attack, or data breach. Triggers when alarm count in the last hour exceeds the 7-day hourly average by more than 3x.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Exfiltration | |
| Impact |
Rule Body
id: 4a7b3c9e-2d15-4e8f-b6a3-9c2e7d5a1b4f
name: SOCRadar Alarm Volume Spike
description: |
'Detects unusual spikes in SOCRadar alarm volume that may indicate an active campaign, coordinated attack, or data breach. Triggers when alarm count in the last hour exceeds the 7-day hourly average by more than 3x.'
severity: Medium
status: Available
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
- Exfiltration
relevantTechniques:
- T1485
- T1567
query: |
let baseline = SOCRadar_Alarms_CL
| where TimeGenerated > ago(7d) and TimeGenerated < ago(1h)
| summarize AvgHourly = count() / 168.0;
let recent = SOCRadar_Alarms_CL
| where TimeGenerated > ago(1h)
| summarize RecentCount = count() by AlarmMainType;
recent
| extend BaselineAvg = toscalar(baseline)
| where RecentCount > (BaselineAvg * 3) and RecentCount > 5
| extend SpikeRatio = round(RecentCount / BaselineAvg, 2)
| extend timestamp = now()
| extend AccountName = AlarmMainType
entityMappings:
- entityType: Malware
fieldMappings:
- identifier: Name
columnName: AccountName
version: 1.0.0
kind: Scheduled
Stages and Predicates
let recent is inlined into the numbered stages below.
Let Binding: baseline
let baseline = SOCRadar_Alarms_CL
| where TimeGenerated > ago(7d) and TimeGenerated < ago(1h)
| summarize AvgHourly = count() / 168.0;
Stages 1 to 3 define let recent as the rule's main pipeline source. Stages 4 to 6 use it.
Stage 1: source
SOCRadar_Alarms_CL
Stage 2: where
| where TimeGenerated > ago(1h)
Stage 3: summarize
| summarize RecentCount = count() by AlarmMainType
Stage 4: extend
recent
| extend BaselineAvg = toscalar(baseline)
Stage 5: where
| where RecentCount > (BaselineAvg * 3) and RecentCount > 5
Stage 6: extend (3 consecutive steps)
| extend SpikeRatio = round(RecentCount / BaselineAvg, 2)
| extend timestamp = now()
| extend AccountName = AlarmMainType
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
RecentCount | gt |
| field:"RecentCount" kind:gt value:"5" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AlarmMainType | summarize |
RecentCount | summarize |
BaselineAvg | extend |
SpikeRatio | extend |
timestamp | extend |
AccountName | extend |
Suspicious Network Beacons - Sysmon
#Below query detects suspicious beaconing activity by analyzing Sysmon network connection events.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | No specific technique |
Telemetry Coverage
| Provider | Record / event type |
|---|---|
| Sysmon | Event ID 3: Network connection |
Rule Body
// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-machine-learning-and-277c4c30304f
// Part-2: https://mergene.medium.com/enterprise-scale-threat-hunting-network-beacon-detection-with-unsupervised-ml-and-kql-part-2-bff46cfc1e7e
//
// Read the blog to understand how this query works and how to analyze the results.
// This query may not be able to detect beacons that have large sleep values like 6h-1d. Refactoring and additional analysis are required.
//
// Query parameters:
let starttime = 1d;
let endtime = 1s;
// Set the minimum beacon sleep. Increase it to get less results. the format is (hour,minute,second.milisecond).
// Be careful when changing the value. run " print ['timespan'] = make_timespan(0, x, y) " to verify you have the correct value set.
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax=toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50; // jitter in percentage. Set to filter out false positives: small threshold means tighter filtering/fewer results.
// Outlier thresholds. 1.5 means the value is a normal outlier, 3 means the value is far far out.
let OutlierThresholdMax = 2; //increase or decrease this value to get more or less results
// Time delta data set can have some outliers. Define how many outliers are acceptable for a beacon. Values between 1 to 3 should be fine.
let OutlierCountMax = 2; // increasing the value provides more results.
// Define how many devices can have the same beacon.
let CompromisedDeviceCountMax = 10; // increasing the value provides more results.
// Function for parsing Sysmon network connection events.
let parse_sysmon_3 = (T:(TimeGenerated:datetime,EventID:int, Source:string,RenderedDescription:string, EventData:string))
{
T
| where TimeGenerated between (ago(starttime)..ago(endtime))
| where Source == "Microsoft-Windows-Sysmon" and EventID == 3
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| where not (ipv4_is_private(tostring(DestinationIp)))
| project TimeGenerated, Computer=tostring(Computer), UserName=tostring(UserName), Image=tostring(Image), DestinationIp=tostring(DestinationIp), DestinationPort=tostring(DestinationPort)
};
// Get all beacon candidates just by jitter filtering.
let BeaconCandidates = materialize (
Event
| invoke parse_sysmon_3()
| summarize hint.strategy=shuffle make_set(TimeGenerated) by Computer, UserName, Image, DestinationIp, DestinationPort
| where array_length(set_TimeGenerated) > TotalEventsThresholdMin and array_length(set_TimeGenerated) < TotalEventsThresholdMax
| project Computer, UserName, Image, DestinationIp, DestinationPort, TimeGenerated=array_sort_asc(set_TimeGenerated)
| mv-apply TimeGenerated to typeof(datetime) on
(
extend nextTimeGenerated = next(TimeGenerated, 1), nextUserName = next(UserName, 1), nextComputer = next(Computer, 1), nextDestinationIp = next(DestinationIp, 1), nextDestinationPort = next(DestinationPort, 1), nextImage = next(Image, 1)
| extend TimeDeltaInSeconds = datetime_diff('second',nextTimeGenerated,TimeGenerated)
| where nextUserName == UserName and nextComputer == Computer and nextDestinationIp == DestinationIp and nextDestinationPort == DestinationPort and nextImage == Image
| project TimeGenerated, TimeDeltaInSeconds, Computer, UserName, Image, DestinationIp, DestinationPort
| summarize count(), min(TimeGenerated), max(TimeGenerated),
Duration=datetime_diff("second", max(TimeGenerated), min(TimeGenerated)),
percentiles(TimeDeltaInSeconds, 5, 25, 50, 75, 95),
TimeDeltaList=make_list(TimeDeltaInSeconds) by Computer, UserName, DestinationIp, DestinationPort
| extend (TimeDeltaInSeconds_min,TimeDeltaInSeconds_min_index,TimeDeltaInSeconds_max,TimeDeltaInSeconds_max_index,TimeDeltaInSeconds_avg,TimeDeltaInSeconds_stdev,TimeDeltaInSeconds_variance)=series_stats(TimeDeltaList)
| extend JitterPercentage = (TimeDeltaInSeconds_stdev/TimeDeltaInSeconds_avg)*100
// Filter out impossible beacons based on jitter threshold defined.
| where JitterPercentage < JitterThreshold
)
)
;
// Get potentially suspicious beacons based on CompromisedDeviceCountMax
let PotentialBeacons = materialize
(
BeaconCandidates
| summarize dcount(Computer) by Image, DestinationIp, DestinationPort
// Filter out beacon destinations if many devices are connecting to the same destination (like windows update)
| where dcount_Computer <= CompromisedDeviceCountMax
| join kind=inner BeaconCandidates on Image, DestinationIp, DestinationPort
| project-away *1
)
;
// Get candidates that can't be beacons based on outlier analysis on the time delta
let ImpossibleBeaconsByTimeDelta = materialize
(
PotentialBeacons
| extend outliers = series_outliers(TimeDeltaList)
| mv-expand TimeDeltaList, outliers to typeof(double)
| where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax) // outlier can be negative or positive.
| summarize count(), make_set(outliers) by Computer, UserName, Image, DestinationIp, DestinationPort
| where count_ > OutlierCountMax
)
;
// Remove ImpossibleBeaconsByTimeDelta from potentially suspicious beacons.
let SuspiciousBeacons = materialize (
PotentialBeacons
| join kind=leftantisemi ImpossibleBeaconsByTimeDelta on Computer, UserName, Image, DestinationIp, DestinationPort
// if the logs have extra information, they can be used for filtering the nonmalicious destinations
| order by JitterPercentage asc, TimeDeltaInSeconds_avg asc
);
// get prevalence data for the destinations(last14d)
let DestinationList =
SuspiciousBeacons
| summarize make_set(DestinationIp)
;
let PrevalanceData =
Event
| where TimeGenerated between (ago(14d) .. ago(endtime)) // analyze the duration before the last beacon connection
| invoke parse_sysmon_3()
| where DestinationIp in (DestinationList)
| summarize hint.strategy=shuffle DestinationPrevalence = dcount(Computer) by DestinationIp
;
// Enrich suspicious beacons with the historical prevalence data for prioritization
SuspiciousBeacons
| join kind=leftouter PrevalanceData on DestinationIp
| sort by DestinationPrevalence asc
| project-reorder DestinationPrevalence
// It's best to enrich IP information with other logs like proxy/firewall to get more info on the IP and apply filtering.
Stages and Predicates
Parameters
let starttime = 1d;
let endtime = 1s;
let TimeDeltaThresholdMin = make_timespan(0,0,0.001);
let TotalEventsThresholdMin = 15;
let TotalEventsThresholdMax = toint(((totimespan(starttime) - totimespan(endtime))/TimeDeltaThresholdMin));
let JitterThreshold = 50;
let OutlierThresholdMax = 2;
let OutlierCountMax = 2;
let CompromisedDeviceCountMax = 10;
let BeaconCandidates, let PotentialBeacons and let SuspiciousBeacons are inlined into the numbered stages below.
Let Binding: parse_sysmon_3
let parse_sysmon_3 = (T:(TimeGenerated:datetime,EventID:int, Source:string,RenderedDescription:string, EventData:string))
{
T
| where TimeGenerated between (ago(starttime)..ago(endtime))
| where Source == "Microsoft-Windows-Sysmon" and EventID == 3
| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| where not (ipv4_is_private(tostring(DestinationIp)))
| project TimeGenerated, Computer=tostring(Computer), UserName=tostring(UserName), Image=tostring(Image), DestinationIp=tostring(DestinationIp), DestinationPort=tostring(DestinationPort)
};
Let Binding: ImpossibleBeaconsByTimeDelta
let ImpossibleBeaconsByTimeDelta = materialize
(
PotentialBeacons
| extend outliers = series_outliers(TimeDeltaList)
| mv-expand TimeDeltaList, outliers to typeof(double)
| where outliers > OutlierThresholdMax or outliers < (-1 * OutlierCountMax)
| summarize count(), make_set(outliers) by Computer, UserName, Image, DestinationIp, DestinationPort
| where count_ > OutlierCountMax
);
Let Binding: DestinationList
let DestinationList = SuspiciousBeacons
| summarize make_set(DestinationIp);
Let Binding: PrevalanceData
let PrevalanceData = Event
| where TimeGenerated between (ago(14d) .. ago(endtime))
| invoke parse_sysmon_3()
| where DestinationIp in (DestinationList)
| summarize hint.strategy=shuffle DestinationPrevalence = dcount(Computer) by DestinationIp;
Stage 1: source
let BeaconCandidates
Stage 2: source
let PotentialBeacons
Stage 3: source
let SuspiciousBeacons
Stage 4: source
let DestinationList
Stage 5: source
let PrevalanceData
Stage 6: source
Event
Stage 7: where
where TimeGenerated between (ago(86400s) .. ago(1s))
Stage 8: where
where EventID == 3 and Source == "Microsoft-Windows-Sysmon"
Stage 9: extend
extend RenderedDescription
Stage 10: extend
extend EventData
Stage 11: mv-expand
mv-expand EventData
Stage 12: evaluate
evaluate
Stage 13: extend
extend Key, Value
Stage 14: evaluate
evaluate
Stage 15: where
where not ((ipv4_is_in_range(DestinationIp, "10.0.0.0/8") or ipv4_is_in_range(DestinationIp, "172.16.0.0/12") or ipv4_is_in_range(DestinationIp, "192.168.0.0/16") or ipv4_is_in_range(DestinationIp, "169.254.0.0/16") or ipv4_is_in_range(DestinationIp, "127.0.0.0/8")))
Stage 16: project
project Computer, DestinationIp, DestinationPort, Image, TimeGenerated, UserName
Stage 17: summarize
summarize by Computer, UserName, Image, DestinationIp, DestinationPort
Stage 18: where
where <unknown:array_length(, set_TimeGenerated)> < TotalEventsThresholdMax and set_TimeGenerated > 15
Stage 19: project
project Computer, DestinationIp, DestinationPort, Image, TimeGenerated, UserName
Stage 20: kusto:mv-apply
kusto:mv-apply JitterPercentage < JitterThreshold and nextComputer =~ Computer and nextDestinationIp =~ DestinationIp and nextDestinationPort =~ DestinationPort and nextImage =~ Image and nextUserName =~ UserName
Stage 21: summarize
summarize by Image, DestinationIp, DestinationPort
Stage 22: where
where dcount_Computer <= 10
Stage 23: join
join kind=inner (BeaconCandidates) on Image, DestinationIp, DestinationPort
Stage 24: project-away
project-away *1
Stage 25: join (negated)
join kind=leftantisemi (ImpossibleBeaconsByTimeDelta) on Computer, UserName, Image, DestinationIp, DestinationPort
Stage 26: sort
sort by JitterPercentage, TimeDeltaInSeconds_avg
Stage 27: join
join kind=leftouter (PrevalanceData) on DestinationIp
Stage 28: sort
sort by DestinationPrevalence
Stage 29: project-reorder
project-reorder
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DestinationIp | cidr_match |
| field:"DestinationIp" kind:cidr_match |
DestinationIp | in |
| field:"DestinationIp" kind:in value:"DestinationList" |
EventID | eq |
| field:"EventID" kind:eq value:"3" |
Source | eq |
| field:"Source" kind:eq value:"Microsoft-Windows-Sysmon" |
dcount_Computer | le |
| field:"dcount_Computer" kind:le value:"10" |
Exclusions
The rule actively suppresses these predicates.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
DestinationIp | summarize |
DestinationPort | summarize |
Image | summarize |
Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups
#This will alert when a user is added to any of the Privileged Groups. For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities. For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Persistence | |
| Privilege Escalation |
Telemetry Coverage
| Platform | Record / event type |
|---|---|
| Azure | Add member to role |
| Azure | Add member to role in PIM requested (permanent) |
Rules Detecting the Same Action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to a New User (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
Rule Body
id: 0a627f29-f0dd-4924-be92-c3d6dac84367
name: Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups
description: |
'This will alert when a user is added to any of the Privileged Groups.
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
- T1078
tags:
- DEV-0537
query: |
let OperationList = dynamic(["Add member to role","Add member to role in PIM requested (permanent)"]);
let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins"]);
AuditLogs
//| where LoggedByService =~ "Core Directory"
| where Category =~ "RoleManagement"
| where OperationName in~ (OperationList)
| mv-expand TargetResources
| extend modProps = parse_json(TargetResources).modifiedProperties
| mv-expand bagexpansion=array modProps
| evaluate bag_unpack(modProps)
| extend displayName = column_ifexists("displayName", "NotAvailable"), newValue = column_ifexists("newValue", "NotAvailable")
| where displayName =~ "Role.WellKnownObjectName"
| extend DisplayName = displayName, GroupName = replace('"','',newValue)
| extend initByApp = parse_json(InitiatedBy).app, initByUser = parse_json(InitiatedBy).user
| extend AppId = initByApp.appId,
InitiatedByDisplayName = case(isnotempty(initByApp.displayName), initByApp.displayName, isnotempty(initByUser.displayName), initByUser.displayName, "not available"),
ServicePrincipalId = tostring(initByApp.servicePrincipalId),
ServicePrincipalName = tostring(initByApp.servicePrincipalName),
UserId = initByUser.id,
UserIPAddress = initByUser.ipAddress,
UserRoles = initByUser.roles,
UserPrincipalName = tostring(initByUser.userPrincipalName),
TargetUserPrincipalName = tostring(TargetResources.userPrincipalName)
| where GroupName in~ (PrivilegedGroups)
// If you don't want to alert for operations from PIM, remove below filtering for MS-PIM.
//| where InitiatedByDisplayName != "MS-PIM" and InitiatedByDisplayName != "MS-PIM-Fairfax"
| project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, AppId, InitiatedByDisplayName, ServicePrincipalId, ServicePrincipalName, DisplayName, GroupName, UserId, UserIPAddress, UserRoles, UserPrincipalName, TargetUserPrincipalName
| extend InitiatorAccount = case(isnotempty(ServicePrincipalName), ServicePrincipalName, isnotempty(ServicePrincipalId), ServicePrincipalId, isnotempty(UserPrincipalName), UserPrincipalName, "not available")
| extend InitiatorName = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',0)[0]),"")
| extend InitiatorUPNSuffix = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',1)[0]),"")
| extend InitiatorAadUserId = iif(InitiatorAccount !has '@',InitiatorAccount,"")
| extend TargetName = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',0)[0]),"")
| extend TargetUPNSuffix = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',1)[0]),"")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatorName
- identifier: UPNSuffix
columnName: InitiatorUPNSuffix
- identifier: AadUserId
columnName: InitiatorAadUserId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
version: 1.0.4
kind: NRT
Stages and Predicates
Parameters
let OperationList = dynamic(["Add member to role","Add member to role in PIM requested (permanent)"]);
let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins"]);
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where OperationName in~ (OperationList)
Stage 4: mv-expand
| mv-expand TargetResources
Stage 5: extend
| extend modProps = parse_json(TargetResources).modifiedProperties
Stage 6: mv-expand
| mv-expand bagexpansion=array modProps
Stage 7: evaluate
| evaluate bag_unpack(modProps)
Stage 8: extend
| extend displayName = column_ifexists("displayName", "NotAvailable"), newValue = column_ifexists("newValue", "NotAvailable")
Stage 9: where
| where displayName =~ "Role.WellKnownObjectName"
Stage 10: extend (3 consecutive steps)
| extend DisplayName = displayName, GroupName = replace('"','',newValue)
| extend initByApp = parse_json(InitiatedBy).app, initByUser = parse_json(InitiatedBy).user
| extend AppId = initByApp.appId,
InitiatedByDisplayName = case(isnotempty(initByApp.displayName), initByApp.displayName, isnotempty(initByUser.displayName), initByUser.displayName, "not available"),
ServicePrincipalId = tostring(initByApp.servicePrincipalId),
ServicePrincipalName = tostring(initByApp.servicePrincipalName),
UserId = initByUser.id,
UserIPAddress = initByUser.ipAddress,
UserRoles = initByUser.roles,
UserPrincipalName = tostring(initByUser.userPrincipalName),
TargetUserPrincipalName = tostring(TargetResources.userPrincipalName)
Stage 11: where
| where GroupName in~ (PrivilegedGroups)
Stage 12: project
| project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, AppId, InitiatedByDisplayName, ServicePrincipalId, ServicePrincipalName, DisplayName, GroupName, UserId, UserIPAddress, UserRoles, UserPrincipalName, TargetUserPrincipalName
Stage 13: extend (6 consecutive steps)
| extend InitiatorAccount = case(isnotempty(ServicePrincipalName), ServicePrincipalName, isnotempty(ServicePrincipalId), ServicePrincipalId, isnotempty(UserPrincipalName), UserPrincipalName, "not available")
| extend InitiatorName = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',0)[0]),"")
| extend InitiatorUPNSuffix = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',1)[0]),"")
| extend InitiatorAadUserId = iif(InitiatorAccount !has '@',InitiatorAccount,"")
| extend TargetName = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',0)[0]),"")
| extend TargetUPNSuffix = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',1)[0]),"")
InitiatorAccount =isnotempty(ServicePrincipalName)ServicePrincipalNameisnotempty(ServicePrincipalId)ServicePrincipalIdisnotempty(UserPrincipalName)UserPrincipalName"not available"Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
GroupName | in |
| field:"GroupName" kind:in |
OperationName | in |
| field:"OperationName" kind:in |
displayName | eq |
| field:"displayName" kind:eq value:"Role.WellKnownObjectName" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AADOperationType | project |
AADTenantId | project |
AppId | project |
Category | project |
DisplayName | project |
GroupName | project |
InitiatedByDisplayName | project |
OperationName | project |
ServicePrincipalId | project |
ServicePrincipalName | project |
TargetUserPrincipalName | project |
TimeGenerated | project |
UserIPAddress | project |
UserId | project |
UserPrincipalName | project |
UserRoles | project |
InitiatorAccount | extend |
InitiatorName | extend |
InitiatorUPNSuffix | extend |
InitiatorAadUserId | extend |
TargetName | extend |
TargetUPNSuffix | extend |
TI Map Domain entity to DnsEvents
#Identifies a match in DnsEvents from any Domain IOC from TI
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control |
Rule Body
id: 03a8e294-3fc7-4d65-9da2-cff91fb5b6dc
name: TI Map Domain entity to DnsEvents
description: |
Identifies a match in DnsEvents from any Domain IOC from TI
severity: Medium
requiredDataConnectors:
- connectorId: DNS
dataTypes:
- DnsEvents
- 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: |
// Define the lookback periods for time-based filters
let dt_lookBack = 1h; // Look back 1 hour for DNS events
let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
// Fetch threat intelligence indicators related to domains
let Domain_Indicators = ThreatIntelIndicators
// Filter out indicators without domain names
| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
| where IndicatorType == "domain-name"
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
| where TimeGenerated >= ago(ioc_lookBack)
| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
| where IsActive and (ValidUntil > now() or isempty(ValidUntil));
// Create a list of TLDs in our threat feed for later validation
let maxListSize = 100000; // Define the maximum allowed size for each list
let list_tlds = Domain_Indicators
| extend parts = split(DomainName, '.')
| extend tld = parts[(array_length(parts)-1)]
| extend IndicatorId = tostring(split(Id, "--")[2])
| summarize count() by tostring(tld)
| project tld
| summarize make_list(tld, maxListSize);
// Perform a join between domain indicators and DNS events to identify potential malicious activity
Domain_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
// Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
| join kind=innerunique (
DnsEvents
| where TimeGenerated > ago(dt_lookBack)
// Extract domain patterns from syslog message
| where isnotempty(Name)
| extend parts = split(Name, '.')
| extend tld = parts[(array_length(parts)-1)]
// Validate parsed domain by checking if the TLD is in the list of TLDs in our threat feed
| where tld in~ (list_tlds)
| extend DNS_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.Name
// Filter out DNS events that occurred after the expiration of the corresponding indicator
| where DNS_TimeGenerated < ValidUntil
// Group the results by IndicatorId and Name, and keep the DNS event with the latest timestamp
| summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by Id, Name
// Select the desired output fields
| extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
| project DNS_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, Url, Computer, ClientIP//, Name, QueryType, Type, DomainName
// Extract hostname and DNS domain from the Computer field
| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
// Rename the timestamp field
| extend timestamp = DNS_TimeGenerated
entityMappings:
- entityType: Host
fieldMappings:
- identifier: FullName
columnName: Computer
- identifier: HostName
columnName: HostName
- identifier: DnsDomain
columnName: DnsDomain
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
- entityType: URL
fieldMappings:
- identifier: Url
columnName: Url
version: 1.4.7
kind: Scheduled
Stages and Predicates
Parameters
let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let maxListSize = 100000;
let Domain_Indicators is inlined into the numbered stages below.
Let Binding: list_tlds
let list_tlds = Domain_Indicators
| extend parts = split(DomainName, '.')
| extend tld = parts[(array_length(parts)-1)]
| extend IndicatorId = tostring(split(Id, "--")[2])
| summarize count() by tostring(tld)
| project tld
| summarize make_list(tld, maxListSize);
Stages 1 to 7 define let Domain_Indicators as the rule's main pipeline source. Stages 8 to 15 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 (3 consecutive steps)
| extend DomainName = tolower(ObservableValue)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend Url = iff(ObservableKey == "url:value", 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: project-reorder
Domain_Indicators
| project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
Stage 9: join
| join kind=innerunique (
DnsEvents
| where TimeGenerated > ago(dt_lookBack)
| where isnotempty(Name)
| extend parts = split(Name, '.')
| extend tld = parts[(array_length(parts)-1)]
| where tld in~ (list_tlds)
| extend DNS_TimeGenerated = TimeGenerated
) on $left.DomainName==$right.Name
Stage 10: where
where DNS_TimeGenerated < ValidUntil
Stage 11: summarize
summarize DNS_TimeGenerated by Id, Name
Stage 12: extend
extend ActivityGroupNames
Stage 13: project
project ActivityGroupNames, ClientIP, Computer, Confidence, DNS_TimeGenerated, Description, Id, Url, ValidUntil
Stage 14: extend
extend DnsDomain, HostName
Stage 15: extend
extend timestamp
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
IndicatorType | eq |
| field:"IndicatorType" kind:eq value:"domain-name" |
IsActive | eq |
| field:"IsActive" kind:eq value:"true" |
Name | is_not_null | field:"Name" kind:is_not_null | |
ValidUntil | is_null | field:"ValidUntil" kind:is_null | |
tld | in |
| field:"tld" kind:in value:"list_tlds" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
ActivityGroupNames | project |
ClientIP | project |
Computer | project |
Confidence | project |
DNS_TimeGenerated | project |
Description | project |
Id | project |
Url | project |
ValidUntil | project |
DnsDomain | extend |
HostName | extend |
timestamp | extend |
Ubiquiti - Unusual FTP connection to external server
#'Detects local to remote (L2R) FTP connections.'
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Command & Control | |
| Exfiltration |
Rule Body
id: fd200125-9d57-4838-85ca-6430c63e4e5d
name: Ubiquiti - Unusual FTP connection to external server
description: |
'Detects local to remote (L2R) FTP connections.'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: CustomLogsAma
dataTypes:
- Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- CommandAndControl
relevantTechniques:
- T1048
- T1071.002
query: |
let allowed_ftp = dynamic(['127.0.0.2']);
UbiquitiAuditEvent
| where EventCategory == 'firewall'
| where ipv4_is_private(SrcIpAddr)
| where ipv4_is_private(DstIpAddr) == 'False'
| where DstPortNumber in ('20', '21')
| where DstIpAddr !in (allowed_ftp)
| extend IPCustomEntity = SrcIpAddr
entityMappings:
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled
Stages and Predicates
Parameters
let allowed_ftp = dynamic(['127.0.0.2']);
Stage 1: source
UbiquitiAuditEvent
Stage 2: where
| where EventCategory == 'firewall'
Stage 3: where
| where ipv4_is_private(SrcIpAddr)
Stage 4: where
| where ipv4_is_private(DstIpAddr) == 'False'
Stage 5: where
| where DstPortNumber in ('20', '21')
Stage 6: where
| where DstIpAddr !in (allowed_ftp)
Stage 7: extend
| extend IPCustomEntity = SrcIpAddr
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
DstPortNumber | in |
| field:"DstPortNumber" kind:in |
EventCategory | eq |
| field:"EventCategory" kind:eq value:"firewall" |
SrcIpAddr | cidr_match |
| field:"src_ip" kind:cidr_match |
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
DstIpAddr | 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:DstIpAddr |
DstIpAddr | eq | allowed_ftp | excludes:DstIpAddr field:"DstIpAddr" value:"allowed_ftp" |
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
IPCustomEntity | extend |
UniFi Site Manager: IPS/IDS disabled or misconfigured
#Identifies when a UniFi gateway threat-protection state transitions away from the expected IPS-active mode, which may indicate admin action or attacker tampering.
MITRE ATT&CK Coverage
| Tactic | Techniques |
|---|---|
| Defense Impairment |
Rule Body
id: 36a64027-729e-51d7-16bf-8e926c03712a
name: "UniFi Site Manager: IPS/IDS disabled or misconfigured"
description: |
Identifies when a UniFi gateway threat-protection state transitions away from the expected IPS-active mode, which may indicate admin action or attacker tampering.
severity: High
status: Available
requiredDataConnectors:
- connectorId: UniFiSiteManagerConnectorDefinition
dataTypes:
- Unifi_SiteManager_Sites_CL
queryFrequency: 15m
queryPeriod: 45m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
subTechniques:
- T1562.001
query: |
let prev = Unifi_SiteManager_Sites_CL
| where TimeGenerated between (ago(30m) .. ago(15m))
| summarize arg_max(TimeGenerated, *) by tostring(SiteId)
| project siteId_s = tostring(SiteId), prevIpsMode = IpsMode, prevInspection = InspectionState;
Unifi_SiteManager_Sites_CL
| where TimeGenerated > ago(15m)
| summarize arg_max(TimeGenerated, *) by tostring(SiteId)
| extend siteId_s = tostring(SiteId)
| join kind=inner prev on siteId_s
| where (IpsMode != 'ips' and prevIpsMode == 'ips')
or (InspectionState != 'on' and prevInspection == 'on')
| extend Activity = strcat('Threat protection degraded - IpsMode: ', prevIpsMode, ' -> ', IpsMode, ', inspection: ', prevInspection, ' -> ', InspectionState)
| project TimeGenerated, SiteId, SiteName, Activity, prevIpsMode, IpsMode, prevInspection, InspectionState
entityMappings:
- entityType: Host
fieldMappings:
- identifier: HostName
columnName: SiteId
- identifier: DnsDomain
columnName: SiteName
incidentConfiguration:
createIncident: true
groupingConfiguration:
enabled: true
reopenClosedIncident: false
lookbackDuration: PT5H
matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1
Stages and Predicates
Let Binding: prev
let prev = Unifi_SiteManager_Sites_CL
| where TimeGenerated between (ago(30m) .. ago(15m))
| summarize arg_max(TimeGenerated, *) by tostring(SiteId)
| project siteId_s = tostring(SiteId), prevIpsMode = IpsMode, prevInspection = InspectionState;
Stage 1: source
Unifi_SiteManager_Sites_CL
Stage 2: where
| where TimeGenerated > ago(15m)
Stage 3: summarize
| summarize arg_max(TimeGenerated, *) by tostring(SiteId)
Stage 4: extend
| extend siteId_s = tostring(SiteId)
Stage 5: join
| join kind=inner prev on siteId_s
Stage 6: where
where ((InspectionState != "on" and prevInspection == "on") or (IpsMode != "ips" and prevIpsMode == "ips"))
Stage 7: extend
extend Activity
Stage 8: project
project Activity, InspectionState, IpsMode, SiteId, SiteName, TimeGenerated, prevInspection, prevIpsMode
Indicators
These rows show field, operator, and value matches.
Output Fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Activity | project |
InspectionState | project |
IpsMode | project |
SiteId | project |
SiteName | project |
TimeGenerated | project |
prevInspection | project |
prevIpsMode | project |