Detection rules › Kusto

Kusto rules: 0b

1Password - User account MFA settings changed

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

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

TacticTechniques
Persistence

Telemetry Coverage

PlatformRecord / event type
1Passwordany: 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.

FieldKindValuesSearch
actionmatch
  • disblmfa transforms: term
  • enblmfa transforms: term
  • updatmfa transforms: term
field:"action" kind:match
log_sourceeq
  • auditevents corpus 12 (kusto 12)
field:"log_source" kind:eq value:"auditevents"
object_typeeq
  • user corpus 2 (kusto 2)
field:"object_type" kind:eq value:"user"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
SrcIpAddrextend

Alsid DCSync

#
Status
available
Severity
high
Time window
2h
Source
github.com/Azure/Azure-Sentinel

'Searches for DCSync attacks'

MITRE ATT&CK Coverage

TacticTechniques
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.

FieldKindValuesSearch
Codenameeq
  • DCSync corpus 3 (kusto 3)
field:"Codename" kind:eq value:"DCSync"
MessageTypeeq
  • 2 corpus 21 (kusto 21)
field:"MessageType" kind:eq value:"2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

ApexOne - Device access permissions was changed

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

'Query shows device access permissions was changed.'

MITRE ATT&CK Coverage

TacticTechniques
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 =
ifDeviceCustomNumber3 == 0"Modify"
elifDeviceCustomNumber3 == 1"Read and execute"
elifDeviceCustomNumber3 == 2"Read"
elifDeviceCustomNumber3 == 3"List device content only"
elifDeviceCustomNumber3 == 4"Block"
else"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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserNamesummarize
Permissionssummarize
PermissionCountextend
AccountCustomEntityextend

ApexOne - Inbound remote access connection

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

'Detects inbound remote access connection.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

Attempt to Update Security Object Failed

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
Endpointproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

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.

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

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.

FieldKindValuesSearch
Findingeq
  • LargeDefaultGroupsPSRemote corpus 2 (kusto 2)
field:"Finding" kind:eq value:"LargeDefaultGroupsPSRemote"

Cross-tenant Access Settings Organization Inbound Collaboration Settings Changed

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

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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.

FieldKindValuesSearch
OperationNamematch
  • Update a partner cross-tenant access setting transforms: term corpus 4 (kusto 4)
field:"OperationName" kind:match value:"Update a partner cross-tenant access setting"
Property.displayNameeq
  • b2bCollaborationInbound transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"b2bCollaborationInbound"
TargetResource.typeeq
  • Policy transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"Policy"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend

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.

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

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

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
ClickJackingDefenceproject
ContentSecurityPolicyproject
CookieXssProtectionproject
DataInjectionDefenceproject
Descriptionproject
Domainproject
DomainStatusproject
FirstSeenproject
LastSeenproject
MissingEPPCodesproject
NetworkIPproject
ProductNameproject
ProviderNameproject
RiskScoreproject
SecureCookieproject
SetCookieHttpsOnlyproject
Softwaresproject
TimeGeneratedproject
TopDomainproject
UIDproject
WebAppFirewallproject
XFrameOptionsproject
X_XssProtectionproject

Cynerio - IoT - Weak password

#
Severity
high
Time window
5h
Source
github.com/Azure/Azure-Sentinel

User signed in using weak credentials

MITRE ATT&CK Coverage

TacticTechniques
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.

Digital Guardian - Exfiltration to online fileshare

#
Status
available
Severity
high
Time window
30m
Group by
SrcUserName
Source
github.com/Azure/Azure-Sentinel

'Detects exfiltration to online fileshare.'

MITRE ATT&CK Coverage

Rule Body

id: f7b6ddef-c1e9-46f0-8539-dbba7fb8a5b8
name: Digital Guardian - Exfiltration to online fileshare
description: |
  'Detects exfiltration to online fileshare.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  let threshold = 10;
  DigitalGuardianDLPEvent
  | where isnotempty(inspected_document)
  | where http_url contains 'dropbox' or http_url contains 'mega.nz'
  | summarize f = dcount(inspected_document) by SrcUserName, bin(TimeGenerated, 30m)
  | where f >= threshold
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

DigitalGuardianDLPEvent

Stage 2: where

| where isnotempty(inspected_document)

Stage 3: where

| where http_url contains 'dropbox' or http_url contains 'mega.nz'

Stage 4: summarize

| summarize f = dcount(inspected_document) by SrcUserName, bin(TimeGenerated, 30m)

Stage 5: where

| where f >= threshold

Stage 6: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
fge
  • 10 transforms: number
field:"f" kind:ge value:"10"
http_urlcontains
  • dropbox
  • mega.nz
field:"http_url" kind:contains
inspected_documentis_not_null
  • (no value, null check)
field:"inspected_document" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcUserNamesummarize
fsummarize
AccountCustomEntityextend

GCP Audit Logs - VPC Flow Logs Disabled

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

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

TacticTechniques
Defense Impairment

Telemetry Coverage

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 =
ifMethodName has "DeleteVpcFlowLogsConfig""Deleted"
elifConfigState == "DISABLED""Disabled"
else"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.

FieldKindValuesSearch
ConfigStateeq
  • DISABLED
field:"ConfigState" kind:eq value:"DISABLED"
MethodNamematch
  • DeleteVpcFlowLogsConfig transforms: term
  • VpcFlowLogsService.DeleteVpcFlowLogsConfig transforms: term
  • VpcFlowLogsService.UpdateVpcFlowLogsConfig transforms: term
field:"gcp::method_name" kind:match
PermissionTypeeq
  • ADMIN_WRITE
field:"PermissionType" kind:eq value:"ADMIN_WRITE"
ServiceNameeq
  • networkmanagement.googleapis.com
field:"ServiceName" kind:eq value:"networkmanagement.googleapis.com"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
Actionproject
AuthEmailproject
CallerIpAddressproject
FlowLogsConfigNameproject
InsertIdproject
LogNameproject
MethodNameproject
Permissionproject
PermissionGrantedproject
PrincipalEmailproject
ProjectIdproject
ResourceNameproject
ServiceNameproject
Severityproject
TimeGeneratedproject

GWorkspace - Unexpected OS update

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

'Detects unexpected OS update.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
EventMessagematch
  • os_updated transforms: term
field:"EventMessage" kind:match value:"os_updated"
NeqValueis_not_null
  • (no value, null check)
field:"NeqValue" kind:is_not_null
OldValueis_not_null
  • (no value, null check)
field:"OldValue" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
VersionCheckextend
AccountCustomEntityextend

Hunt for public facing devices and exposed ports over time

#
Group by
DeviceName, InternetFacingLocalIp, InternetFacingLocalPort, InternetFacingTransportProtocol
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

Find public facing devices over time via the public device tag in the DeviceInfo table.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

References

Telemetry Coverage

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.

FieldSource
InternetFacingLocalPortsummarize

Imperva - Multiple user agents from same source

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

''Detects suspicious number of user agents from the same IP address.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
d_uasge
  • 10 transforms: number
field:"d_uas" kind:ge value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
d_uassummarize
IPCustomEntityextend

Imperva - Possible command injection

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

'Detects requests with commands in URI.'

MITRE ATT&CK Coverage

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 used in Stage 2

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.

FieldKindValuesSearch
QueryStringmatch
  • %2fbin%2fbash transforms: term
  • %2fetc%2fpasswd transforms: term
  • %2fetc%2fshadow transforms: term
  • curl transforms: term
  • exec( transforms: term
  • gcc transforms: term
  • hostname transforms: term
  • ifconfig transforms: term
  • phpinfo transforms: term
  • ping transforms: term
  • python transforms: term
  • rout transforms: term
  • systeminfo transforms: term
  • uname transforms: term
  • wget transforms: term
  • whoami transforms: term
field:"QueryString" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Lumen TI IPAddress in SigninLogs

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

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

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

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.

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

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
Confidenceproject
EndTimeproject
IPAddressproject
Idproject
ResultTypeproject
SL_ipEntityproject
StartTimeproject
TI_ipEntityproject
Tagsproject
Typeproject
UserPrincipalNameproject
ValidUntilproject
timestampproject

NGINX - Command in URI

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

'Detects command in URI'

MITRE ATT&CK Coverage

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 used in Stage 2

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.

FieldKindValuesSearch
UrlOriginalmatch
  • %26%20%64%69%72 transforms: term
  • %26%64%69%72 transforms: term
  • %2f%62%69%6e%2f%62%61%73%68 transforms: term
  • %2f%62%69%6e%2f%62%61%73%68%20%2d%63%27 transforms: term
  • %2f%75%73%72%2f%62%69%6e%2f%69%64 transforms: term
  • %3b%69%64 transforms: term
  • %3c%73%63%72%69%70%74%3e transforms: term
  • %63%75%72%6c transforms: term
  • %65%76%61%6c%28 transforms: term
  • %65%78%65%63%28 transforms: term
  • %70%68%70%69%6e%66%6f%28%29 transforms: term
  • %70%79%74%68%6f%6e transforms: term
  • %73%6c%65%65%70%28 transforms: term
  • %73%79%73%74%65%6d%69%6e%66%6f transforms: term
  • %75%6e%61%6d%65 transforms: term
  • %77%67%65%74 transforms: term
  • %77%68%6f%61%6d%69 transforms: term
  • & dir transforms: term
  • &dir transforms: term
  • /bin/bash transforms: term
  • /bin/bash -c transforms: term
  • /etc/passwd transforms: term
  • /usr/bin/id( transforms: term
  • ;id transforms: term
  • <script> transforms: term
  • cat%20/etc/passwd transforms: term
  • curl transforms: term
  • eval( transforms: term
  • exec( transforms: term
  • gcc transforms: term
  • phpinfo() transforms: term
  • ping -i transforms: term
  • python transforms: term
  • sleep( transforms: term
  • systeminfo transforms: term
  • uname transforms: term
  • wget transforms: term
  • whoami transforms: term
field:"UrlOriginal" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
UrlCustomEntityextend

OCI - Inbound SSH connection

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

'Detects inbound SSH connection.'

MITRE ATT&CK Coverage

TacticTechniques
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.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

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.

Status
available
Severity
medium
Time window
7d
Group by
AlarmMainType
Source
github.com/Azure/Azure-Sentinel

'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

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 used in Stage 4

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.

FieldKindValuesSearch
RecentCountgt
  • 5 transforms: number
field:"RecentCount" kind:gt value:"5"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlarmMainTypesummarize
RecentCountsummarize
BaselineAvgextend
SpikeRatioextend
timestampextend
AccountNameextend

Suspicious Network Beacons - Sysmon

#
Group by
Computer, DestinationIp, DestinationPort, Image, UserName
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

Below query detects suspicious beaconing activity by analyzing Sysmon network connection events.

MITRE ATT&CK Coverage

TacticTechniques
Command & ControlNo specific technique

Telemetry Coverage

ProviderRecord / event type
SysmonEvent 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 used in Stage 25

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 used in Stage 4

let DestinationList = SuspiciousBeacons
    | summarize make_set(DestinationIp);

Let Binding: PrevalanceData used in Stages 5, 27

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.

FieldKindValuesSearch
DestinationIpcidr_match
  • 10.0.0.0/8 transforms: tostring
  • 127.0.0.0/8 transforms: tostring
  • 169.254.0.0/16 transforms: tostring
  • 172.16.0.0/12 transforms: tostring
  • 192.168.0.0/16 transforms: tostring
field:"DestinationIp" kind:cidr_match
DestinationIpin
  • DestinationList
field:"DestinationIp" kind:in value:"DestinationList"
EventIDeq
  • 3
field:"EventID" kind:eq value:"3"
Sourceeq
  • Microsoft-Windows-Sysmon
field:"Source" kind:eq value:"Microsoft-Windows-Sysmon"
dcount_Computerle
  • 10 transforms: number
field:"dcount_Computer" kind:le value:"10"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationIpcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DestinationIp
outliersgt2excludes:outliers field:"outliers" value:"2"
outlierslt-2excludes:outliers field:"outliers" value:"-2"
EventIDeq3excludes:EventID field:"EventID" value:"3"
JitterPercentagecross_field_compareJitterThresholdexcludes:JitterPercentage field:"JitterPercentage" value:"JitterThreshold"
count_gt2excludes:count_ field:"count_" value:"2"
dcount_Computerle10excludes:dcount_Computer field:"dcount_Computer" value:"10"
nextComputercross_field_compareComputerexcludes:nextComputer field:"nextComputer" value:"Computer"
nextDestinationIpcross_field_compareDestinationIpexcludes:nextDestinationIp field:"nextDestinationIp" value:"DestinationIp"
nextDestinationPortcross_field_compareDestinationPortexcludes:nextDestinationPort field:"nextDestinationPort" value:"DestinationPort"
nextImagecross_field_compareImageexcludes:nextImage field:"nextImage" value:"Image"
nextUserNamecross_field_compareUserNameexcludes:nextUserName field:"nextUserName" value:"UserName"
set_TimeGeneratedcross_field_compareTotalEventsThresholdMaxexcludes:set_TimeGenerated field:"set_TimeGenerated" value:"TotalEventsThresholdMax"
set_TimeGeneratedgt15excludes:set_TimeGenerated field:"set_TimeGenerated" value:"15"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DestinationIpsummarize
DestinationPortsummarize
Imagesummarize

Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups

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

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

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

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 =
ifisnotempty(ServicePrincipalName)ServicePrincipalName
elifisnotempty(ServicePrincipalId)ServicePrincipalId
elifisnotempty(UserPrincipalName)UserPrincipalName
else"not available"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
GroupNamein
  • PrivilegedRoleAdmins
  • TenantAdmins
  • UserAccountAdmins
field:"GroupName" kind:in
OperationNamein
  • Add member to role corpus 3 (kusto 3)
  • Add member to role in PIM requested (permanent)
field:"OperationName" kind:in
displayNameeq
  • Role.WellKnownObjectName
field:"displayName" kind:eq value:"Role.WellKnownObjectName"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADOperationTypeproject
AADTenantIdproject
AppIdproject
Categoryproject
DisplayNameproject
GroupNameproject
InitiatedByDisplayNameproject
OperationNameproject
ServicePrincipalIdproject
ServicePrincipalNameproject
TargetUserPrincipalNameproject
TimeGeneratedproject
UserIPAddressproject
UserIdproject
UserPrincipalNameproject
UserRolesproject
InitiatorAccountextend
InitiatorNameextend
InitiatorUPNSuffixextend
InitiatorAadUserIdextend
TargetNameextend
TargetUPNSuffixextend

TI Map Domain entity to DnsEvents

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

Identifies a match in DnsEvents from any Domain IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
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 used in Stage 9

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
Computerproject
Confidenceproject
DNS_TimeGeneratedproject
Descriptionproject
Idproject
Urlproject
ValidUntilproject
DnsDomainextend
HostNameextend
timestampextend

Ubiquiti - Unusual FTP connection to external server

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

'Detects local to remote (L2R) FTP connections.'

MITRE ATT&CK Coverage

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.

FieldKindValuesSearch
DstPortNumberin
  • 20
  • 21
field:"DstPortNumber" kind:in
EventCategoryeq
  • firewall
field:"EventCategory" kind:eq value:"firewall"
SrcIpAddrcidr_match
  • 10.0.0.0/8
  • 127.0.0.0/8
  • 169.254.0.0/16
  • 172.16.0.0/12
  • 192.168.0.0/16
field:"src_ip" kind:cidr_match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DstIpAddrcidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:DstIpAddr
DstIpAddreqallowed_ftpexcludes:DstIpAddr field:"DstIpAddr" value:"allowed_ftp"

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

UniFi Site Manager: IPS/IDS disabled or misconfigured

#
Status
available
Severity
high
Time window
45m
Group by
siteId_s
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
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 used in Stage 5

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.

FieldSource
Activityproject
InspectionStateproject
IpsModeproject
SiteIdproject
SiteNameproject
TimeGeneratedproject
prevInspectionproject
prevIpsModeproject

Back to all Kusto rules