Detection rules › Kusto

CTE - Device Repeatedly Blocked Across Multiple Days

Severity
medium
Time window
7d
Group by
DeviceId, DeviceOs, DeviceUser, HostName, TenantId
Source
github.com/Azure/Azure-Sentinel

A device has triggered URL blocks on 3 or more distinct days in the past week, suggesting a recurring risky browsing pattern or persistent exposure.

MITRE ATT&CK coverage

TacticTechniques
Initial Access

Rule body

id: 8fbb095d-3a35-483b-b264-25b858a52f4d
name: CTE - Device Repeatedly Blocked Across Multiple Days
description: |
  A device has triggered URL blocks on 3 or more distinct days in the past week, suggesting a recurring risky browsing pattern or persistent exposure.
severity: Medium
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 12h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1189
query: |
  CybereinforceCTE_CL
  | where EventType == "block.url"
  | extend d = parse_json(Details)
  | extend
      DeviceId = tostring(d.deviceId),
      HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
      DeviceUser = tostring(d.deviceUser),
      DeviceOs = tostring(d.deviceOs),
      DeviceVersion = tostring(d.deviceVersion),
      RuleId = tostring(d.ruleId),
      RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
      Pattern = tostring(d.pattern),
      Url = tostring(d.url),
      Action = tostring(d.action),
      IsIoc = tobool(d.ioc),
      ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
      ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
      UserAgent = tostring(d.userAgent)
  | summarize BlockDays=dcount(startofday(TimeGenerated)), BlockCount=count(), SampleUrls=make_set(Url, 20) by DeviceId, HostName, DeviceUser, DeviceOs, TenantId
  | where BlockDays >= 3
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "block.url"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend
    DeviceId = tostring(d.deviceId),
    HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
    DeviceUser = tostring(d.deviceUser),
    DeviceOs = tostring(d.deviceOs),
    DeviceVersion = tostring(d.deviceVersion),
    RuleId = tostring(d.ruleId),
    RuleName = iff(isnotempty(tostring(d.ruleName)), tostring(d.ruleName), "Pattern Match"),
    Pattern = tostring(d.pattern),
    Url = tostring(d.url),
    Action = tostring(d.action),
    IsIoc = tobool(d.ioc),
    ClientIp = tostring(split(tostring(d.clientIp), ":")[0]),
    ClientPort = tostring(split(tostring(d.clientIp), ":")[1]),
    UserAgent = tostring(d.userAgent)
HostName =
if/* macro: isnotempty(tostring(d.hostname)) */tostring(d.hostname)
elsestrcat("cte-device-", substring(EntityId, 0, 8))
RuleName =
if/* macro: isnotempty(tostring(d.ruleName)) */tostring(d.ruleName)
else"Pattern Match"

Stage 5: summarize

| summarize BlockDays=dcount(startofday(TimeGenerated)), BlockCount=count(), SampleUrls=make_set(Url, 20) by DeviceId, HostName, DeviceUser, DeviceOs, TenantId

Stage 6: where

| where BlockDays >= 3

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
BlockCountsummarize
BlockDayssummarize
DeviceIdsummarize
DeviceOssummarize
DeviceUsersummarize
HostNamesummarize
SampleUrlssummarize
TenantIdsummarize