Detection rules › Kusto

Kusto rules: 1u

Backup Proxy Deleted

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

Detects when a backup proxy is deleted from Veeam Backup & Replication. This might indicate unauthorized removal of critical components.

Rule Body

id: b8527d36-4cba-49c1-9cab-de9cdc3de879
name: Backup Proxy Deleted
description: Detects when a backup proxy is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 27900\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\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 == 27900

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
  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
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

BloodHound Attack Path Finding - AllExtended Privileges on Tier Zero Objects

#

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 - AllExtended Privileges on Tier Zero Objects

Rule Body

id: 234ba956-2361-4db2-a9d4-c4c89fb2ed55
name: BloodHound Attack Path Finding - AllExtended Privileges on Tier Zero Objects
description: BloodHound Attack Path Finding - AllExtended Privileges on Tier Zero
  Objects
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 == "T0AllExtendedRights"
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 == "T0AllExtendedRights"

Indicators

These rows show field, operator, and value matches.

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

Cisco Umbrella - Connection to non-corporate private network

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

'IP addresses of broadband links that usually indicates users attempting to access their home network, for example for a remote session to a home computer.'

MITRE ATT&CK Coverage

TacticTechniques
Command & ControlNo specific technique
ExfiltrationNo specific technique

Rule Body

id: c9b6d281-b96b-4763-b728-9a04b9fe1246
name: Cisco Umbrella - Connection to non-corporate private network
description: |
  'IP addresses of broadband links that usually indicates users attempting to access their home network, for example for a remote session to a home computer.'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes:
      - Cisco_Umbrella_proxy_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Exfiltration
query: |
  let lbtime = 10m;
  Cisco_Umbrella
  | where TimeGenerated > ago(lbtime)
  | where EventType == 'proxylogs'
  | where DvcAction =~ 'Allowed'
  | where UrlCategory has_any ('Dynamic and Residential', 'Personal VPN')
  | project TimeGenerated, SrcIpAddr, Identities
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Identities
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.1.1
kind: Scheduled

Stages and Predicates

Parameters

let lbtime = 10m;

Stage 1: source

Cisco_Umbrella

Stage 2: where

| where TimeGenerated > ago(lbtime)

Stage 3: where

| where EventType == 'proxylogs'

Stage 4: where

| where DvcAction =~ 'Allowed'

Stage 5: where

| where UrlCategory has_any ('Dynamic and Residential', 'Personal VPN')

Stage 6: project

| project TimeGenerated, SrcIpAddr, Identities

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DvcActioneq
  • Allowed
field:"DvcAction" kind:eq value:"Allowed"
EventTypeeq
  • proxylogs
field:"EventType" kind:eq value:"proxylogs"
UrlCategorymatch
  • Dynamic and Residential transforms: term
  • Personal VPN transforms: term
field:"UrlCategory" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
Identitiesproject
SrcIpAddrproject
TimeGeneratedproject

Corelight - Forced External Outbound SMB

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

'Detects SMB requests that originate internally and communicate with an external IP address.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 73f23aa2-5cc4-4507-940b-75c9092e9e01
name: Corelight - Forced External Outbound SMB
description: |
  'Detects SMB requests that originate internally and communicate with an external IP address.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Corelight
    dataTypes:
      - corelight_conn
      - corelight_conn_red
      - corelight_conn_long
      - corelight_conn_agg
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1187
query: |
  union isfuzzy=true corelight_conn, corelight_conn_agg
  | where local_orig == true
  | where local_resp == false
  | where history hasprefix 'Sh' and (service_dynamic hasprefix 'smb' or service_string hasprefix 'smb')
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: id_orig_h
version: 2.2.0
kind: Scheduled

Stages and Predicates

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: corelight_conn, corelight_conn_agg

Leg 1: corelight_conn

Leg 2: corelight_conn_agg

Applied to the Combined Result

| where local_orig == true | where local_resp == false | where history hasprefix 'Sh' and (service_dynamic hasprefix 'smb' or service_string hasprefix 'smb')

Indicators

These rows show field, operator, and value matches.

Cyble Vision Alerts Pastebin

#

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
low
Time window
30m
Source
github.com/Azure/Azure-Sentinel

'Cyble detected a paste containing references to monitored keywords or domains. Pastebin/Gist exposure may indicate data leakage or threat actor activity referencing the organization.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: dd37e041-3973-482a-aa8c-f484b4178940
name: Cyble Vision Alerts Pastebin
description: |
  'Cyble detected a paste containing references to monitored keywords or domains. Pastebin/Gist exposure may indicate data leakage or threat actor activity referencing the organization.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Reconnaissance
relevantTechniques:
- T1591
query: |
  Alerts_pastebin  
  | where Service == "pastebin" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: Pastebin/Gist Exposure for Keyword {{KeywordName}}
  alertDescriptionFormat: |
    A public paste has been detected referencing monitored keyword {{KeywordName}}. Paste Author {{PB_Author}}. URL {{PB_Data_URL}}. This may indicate sensitive information exposure or monitoring by external actors.
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  PasteAuthor: PB_Author
  PasteUpdatedOn: PB_ContentUpdatedOn
  PasteURL: PB_Data_URL
  PasteID: PB_Data_PasteID
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: MA_Domain
- entityType: URL
  fieldMappings:
  - identifier: Url
    columnName: MA_DomainURL
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: KeywordName
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: Domain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_pastebin

Stage 2: where

| where Service == "pastebin"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Cyble Vision Alerts Social Media Monitoring

#

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
low
Time window
30m
Source
github.com/Azure/Azure-Sentinel

'Detects and CybleVision 'social_media_monitoring' alerts into a single incident.'

MITRE ATT&CK Coverage

Rule Body

id: 231c2c16-3742-4cfb-a8e1-c1a7d09f080a
name: Cyble Vision Alerts Social Media Monitoring
description: |
  'Detects and CybleVision 'social_media_monitoring' alerts into a single incident.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: false
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- Reconnaissance
- ResourceDevelopment
relevantTechniques:
- T1595
- T1585
- T1589
subTechniques: []
query: |
  Alerts_social_media_monitoring 
  | where Service == "social_media_monitoring"
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: CybleVision Social Media Monitoring ({{SM_CreatorName}})
  alertDescriptionFormat: |
    Social media monitoring alert detected. Category {{SM_Category}}. Source {{SM_SourceData}}. Creator {{SM_CreatorName}}.
  alertDynamicProperties: []
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  CreatorName: SM_CreatorName
  Category: SM_Category
  Source: SM_SourceData
  Sentiment: SM_SentimentData
  ContentHash: SM_ContentHash
  Domains: SM_Domains
  Subdomains: SM_Subdomains
  URLS: SM_Urls
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: SM_CreatorName
- entityType: URL
  fieldMappings:
  - identifier: Url
    columnName: SM_Urls
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: Domain
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: IP
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_social_media_monitoring

Stage 2: where

| where Service == "social_media_monitoring"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Serviceeq
  • social_media_monitoring
field:"Service" kind:eq value:"social_media_monitoring"

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Email access via active sync

#
Severity
medium
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query detects attempts to add attacker devices as allowed IDs for active sync using the Set-CASMailbox command. This technique was seen in relation to Solorigate attack but the results can indicate potential malicious activity used in different attacks. - Note that this query can be changed to use the KQL "has_all" operator, which hasn't yet been documented officially, but will be soon. In short, "has_all" will only match when the referenced field has all strings in the list. - Refer to Set-CASMailbox syntax: https://docs.microsoft.com/powershell/module/exchange/set-casmailbox?view=exchange-ps

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 2f561e20-d97b-4b13-b02d-18b34af6e87c
name: Email access via active sync
description: |
   This query detects attempts to add attacker devices as allowed IDs for active sync using the Set-CASMailbox command.
   This technique was seen in relation to Solorigate attack but the results can indicate potential malicious activity used in different attacks.
   - Note that this query can be changed to use the KQL "has_all" operator, which hasn't yet been documented officially, but will be soon.
     In short, "has_all" will only match when the referenced field has all strings in the list.
   - Refer to Set-CASMailbox syntax: https://docs.microsoft.com/powershell/module/exchange/set-casmailbox?view=exchange-ps  
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1068
  - T1078
tags:
  - Solorigate
  - NOBELIUM
query: |
  let timeframe = 1d;
  let cmdList = dynamic(["Set-CASMailbox","ActiveSyncAllowedDeviceIDs","add"]);
  (union isfuzzy=true
  (
  SecurityEvent
  | where TimeGenerated >= ago(timeframe)
  | where EventID == 4688
  | where CommandLine has_all (cmdList)
  | project Type, TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, Process, ParentProcessName, CommandLine
  | extend timestamp = TimeGenerated, AccountEntity = Account, HostEntity = Computer
  ),
  ( WindowsEvent
  | where TimeGenerated >= ago(timeframe)
  | where EventID == 4688
  | where EventData has_all (cmdList)
  | extend CommandLine = tostring(EventData.CommandLine) 
  | where CommandLine has_all (cmdList)
  | extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend SubjectUserName = tostring(EventData.SubjectUserName)
  | extend SubjectDomainName = tostring(EventData.SubjectDomainName)
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend Process=tostring(split(NewProcessName, '\\')[-1])
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | project Type, TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, Process, ParentProcessName, CommandLine
  | extend timestamp = TimeGenerated, AccountEntity = Account, HostEntity = Computer
  ),
  (
  DeviceProcessEvents
  | where TimeGenerated >= ago(timeframe)
  | where InitiatingProcessCommandLine has_all (cmdList)
  | project Type, TimeGenerated, DeviceName, AccountName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessParentFileName,  InitiatingProcessCommandLine
  | extend timestamp = TimeGenerated, AccountDomain = InitiatingProcessAccountDomain, AccountName = InitiatingProcessAccountName, HostEntity = DeviceName
  ),
  (
  Event
  | where TimeGenerated > ago(timeframe)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | 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 TimeGenerated >= ago(timeframe)
  | where CommandLine has_all (cmdList)
  | extend Type = strcat(Type, ": ", Source)
  | project Type, TimeGenerated, Computer, User, Process, ParentImage, CommandLine
  | extend timestamp = TimeGenerated, AccountEntity = User, HostEntity = Computer
  )
  )
  | extend HostName = tostring(split(HostEntity, ".")[0]), DomainIndex = toint(indexof(HostEntity, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(HostEntity, DomainIndex + 1), HostEntity)
  | extend AccountName = tostring(split(AccountEntity, @'\')[1]), AccountDomain = tostring(split(AccountEntity, @'\')[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountEntity
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostEntity
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.2.1
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection" ]

Stages and Predicates

Parameters

let timeframe = 1d;
let cmdList = dynamic(["Set-CASMailbox","ActiveSyncAllowedDeviceIDs","add"]);

Union isfuzzy=true (4 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent, DeviceProcessEvents, Event

Leg 1: SecurityEvent

SecurityEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4688
| where CommandLine has_all (cmdList)
| project Type, TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, Process, ParentProcessName, CommandLine
| extend timestamp = TimeGenerated, AccountEntity = Account, HostEntity = Computer

Leg 2: WindowsEvent

WindowsEvent
| where TimeGenerated >= ago(timeframe)
| where EventID == 4688
| where EventData has_all (cmdList)
| extend CommandLine = tostring(EventData.CommandLine) 
| where CommandLine has_all (cmdList)
| extend Account =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend SubjectUserName = tostring(EventData.SubjectUserName)
| extend SubjectDomainName = tostring(EventData.SubjectDomainName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| project Type, TimeGenerated, Computer, Account, SubjectDomainName, SubjectUserName, Process, ParentProcessName, CommandLine
| extend timestamp = TimeGenerated, AccountEntity = Account, HostEntity = Computer

Leg 3: DeviceProcessEvents

DeviceProcessEvents
| where TimeGenerated >= ago(timeframe)
| where InitiatingProcessCommandLine has_all (cmdList)
| project Type, TimeGenerated, DeviceName, AccountName, InitiatingProcessAccountDomain, InitiatingProcessAccountName, InitiatingProcessFileName, InitiatingProcessParentFileName,  InitiatingProcessCommandLine
| extend timestamp = TimeGenerated, AccountDomain = InitiatingProcessAccountDomain, AccountName = InitiatingProcessAccountName, HostEntity = DeviceName

Leg 4: Event

Event
| where TimeGenerated > ago(timeframe)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| 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 TimeGenerated >= ago(timeframe)
| where CommandLine has_all (cmdList)
| extend Type = strcat(Type, ": ", Source)
| project Type, TimeGenerated, Computer, User, Process, ParentImage, CommandLine
| extend timestamp = TimeGenerated, AccountEntity = User, HostEntity = Computer

Applied to the Combined Result

| extend HostName = tostring(split(HostEntity, ".")[0]), DomainIndex = toint(indexof(HostEntity, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(HostEntity, DomainIndex + 1), HostEntity)
| extend AccountName = tostring(split(AccountEntity, @'\')[1]), AccountDomain = tostring(split(AccountEntity, @'\')[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • ActiveSyncAllowedDeviceIDs transforms: term
  • Set-CASMailbox transforms: term
  • add transforms: term corpus 2 (kusto 2)
field:"CommandLine" kind:match
EventDatamatch
  • ActiveSyncAllowedDeviceIDs transforms: term
  • Set-CASMailbox transforms: term
  • add transforms: term
field:"EventData" kind:match
EventIDeq
  • 1 corpus 12 (kusto 11, elastic 1)
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq
InitiatingProcessCommandLinematch
  • ActiveSyncAllowedDeviceIDs transforms: term
  • Set-CASMailbox transforms: term
  • add transforms: term corpus 3 (kusto 3)
field:"ParentCommandLine" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLineproject
Computerproject
ParentImageproject
Processproject
TimeGeneratedproject
Typeproject
Userproject
AccountEntityextend
HostEntityextend
timestampextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountDomainextend
AccountNameextend

Guardian- Content Safety Profanity Policy Violation Detection

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

'This alert creates an incident when Content Safety Profanity Policy Violation detected from the Guardian.'

Rule Body

id: de3ce72f-7826-4be1-ab1a-87a8299c54f6
name: Guardian- Content Safety Profanity Policy Violation Detection
description: |
  'This alert creates an incident when Content Safety Profanity Policy Violation detected from the Guardian.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'JSON'
  | where Severity =~ 'Low'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Content Safety Profanity Policy Violation detection.
  alertDescriptionFormat: |
    This query detects Content Safety Profanity Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'JSON'

Stage 3: where

| where Severity =~ 'Low'

Indicators

These rows show field, operator, and value matches.

Midnight Blizzard - Script payload stored in Registry

#
Severity
medium
Time window
1d
Author
Shain
Source
github.com/Azure/Azure-Sentinel

This query identifies when a process execution command-line indicates that a registry value is written to allow for later execution a malicious script References: https://www.microsoft.com/security/blog/2021/03/04/goldmax-goldfinder-sibot-analyzing-nobelium-malware/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 00cb180c-08a8-4e55-a276-63fb1442d5b5
name: Midnight Blizzard - Script payload stored in Registry
description: |
  'This query identifies when a process execution command-line indicates that a registry value is written to allow for later execution a malicious script
   References: https://www.microsoft.com/security/blog/2021/03/04/goldmax-goldfinder-sibot-analyzing-nobelium-malware/'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
tags:
  - Midnight Blizzard
query: |
  let cmdTokens0 = dynamic(['vbscript','jscript']);
  let cmdTokens1 = dynamic(['mshtml','RunHTMLApplication']);
  let cmdTokens2 = dynamic(['Execute','CreateObject','RegRead','window.close']);
  (union isfuzzy=true 
  (SecurityEvent
  | where TimeGenerated >= ago(14d)
  | where EventID == 4688
  | where CommandLine has @'\Microsoft\Windows\CurrentVersion'
  | where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
  // If you are receiving false positives, then it may help to make the query more strict by uncommenting one or both of the lines below to refine the matches
  //| where CommandLine has_any (cmdTokens0)
  //| where CommandLine has_all (cmdTokens1)
  | where CommandLine has_all (cmdTokens2)
  | project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId
  ),
  (WindowsEvent
  | where TimeGenerated >= ago(14d)
  | where EventID == 4688 and EventData has_all(cmdTokens2) and  EventData has @'\Microsoft\Windows\CurrentVersion'
  | where not(EventData has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
  | extend CommandLine = tostring(EventData.CommandLine)
  | where CommandLine has @'\Microsoft\Windows\CurrentVersion'
  | where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
  // If you are receiving false positives, then it may help to make the query more strict by uncommenting one or both of the lines below to refine the matches
  //| where CommandLine has_any (cmdTokens0)
  //| where CommandLine has_all (cmdTokens1)
  | where CommandLine has_all (cmdTokens2)
  | extend Account =  strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
  | extend NewProcessName = tostring(EventData.NewProcessName)
  | extend Process=tostring(split(NewProcessName, '\\')[-1])
  | extend ParentProcessName = tostring(EventData.ParentProcessName)  
  | project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId)
  | extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
  | extend DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), HostName = tostring(split(Computer, '.', 0)[0]))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.1.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Shain
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Intelligence" ]

Stages and Predicates

Parameters

let cmdTokens0 = dynamic(['vbscript','jscript']);
let cmdTokens1 = dynamic(['mshtml','RunHTMLApplication']);
let cmdTokens2 = dynamic(['Execute','CreateObject','RegRead','window.close']);

Union isfuzzy=true (2 Sources)

Each leg below queries one source; the rule matches if any leg does. Sources: SecurityEvent, WindowsEvent

Leg 1: SecurityEvent

SecurityEvent
| where TimeGenerated >= ago(14d)
| where EventID == 4688
| where CommandLine has @'\Microsoft\Windows\CurrentVersion'
| where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
| where CommandLine has_all (cmdTokens2)
| project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId

Leg 2: WindowsEvent

WindowsEvent
| where TimeGenerated >= ago(14d)
| where EventID == 4688 and EventData has_all(cmdTokens2) and  EventData has @'\Microsoft\Windows\CurrentVersion'
| where not(EventData has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has @'\Microsoft\Windows\CurrentVersion'
| where not(CommandLine has_any (@'\Software\Microsoft\Windows\CurrentVersion\Run', @'\Software\Microsoft\Windows\CurrentVersion\RunOnce'))
| where CommandLine has_all (cmdTokens2)
| extend Account =  strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
| extend NewProcessName = tostring(EventData.NewProcessName)
| extend Process=tostring(split(NewProcessName, '\\')[-1])
| extend ParentProcessName = tostring(EventData.ParentProcessName)  
| project TimeGenerated, Computer, Account, Process, NewProcessName, CommandLine, ParentProcessName, _ResourceId

Applied to the Combined Result

| extend Name = tostring(split(Account, "\\")[1]), NTDomain = tostring(split(Account, "\\")[0])
| extend DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), HostName = tostring(split(Computer, '.', 0)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CommandLinematch
  • CreateObject transforms: term
  • Execute transforms: term corpus 3 (kusto 3)
  • RegRead transforms: term corpus 3 (kusto 3)
  • \Microsoft\Windows\CurrentVersion transforms: term
  • window.close transforms: term corpus 3 (kusto 3)
field:"CommandLine" kind:match
EventDatamatch
  • CreateObject transforms: term
  • Execute transforms: term corpus 2 (kusto 2)
  • RegRead transforms: term corpus 2 (kusto 2)
  • \Microsoft\Windows\CurrentVersion transforms: term
  • window.close transforms: term corpus 2 (kusto 2)
field:"EventData" kind:match
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CommandLinematch\Software\Microsoft\Windows\CurrentVersion\Run, \Software\Microsoft\Windows\CurrentVersion\RunOnceexcludes:CommandLine field:"CommandLine" value:"\Software\Microsoft\Windows\CurrentVersion\Run" field:"CommandLine" value:"\Software\Microsoft\Windows\CurrentVersion\RunOnce"
EventDatamatch\Software\Microsoft\Windows\CurrentVersion\Run, \Software\Microsoft\Windows\CurrentVersion\RunOnceexcludes:EventData field:"EventData" value:"\Software\Microsoft\Windows\CurrentVersion\Run" field:"EventData" value:"\Software\Microsoft\Windows\CurrentVersion\RunOnce"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
CommandLineproject
Computerproject
NewProcessNameproject
ParentProcessNameproject
Processproject
TimeGeneratedproject
_ResourceIdproject
NTDomainextend
Nameextend
DnsDomainextend
HostNameextend

Mimecast Audit - Logon Authentication Failed

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

Detects threat when logon authentication failure found in audit

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: f00197ab-491f-41e7-9e22-a7003a4c1e54
name: Mimecast Audit - Logon Authentication Failed
description: Detects threat when logon authentication failure found in audit
displayName: Mimecast Audit - Logon Authentication Failed
severity: High
requiredDataConnectors:
  - connectorId: MimecastAuditAPI
    dataTypes:
      - MimecastAudit  
enabled: true
query: |
  MimecastAudit 
  | where ['Source IP'] !="" and ['Audit Type'] == "Logon Authentication Failed"
  | extend   SourceIp = ['Source IP'] 
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 3
suppressionDuration: 5h
suppressionEnabled: false
tactics:
- Discovery
- InitialAccess
- CredentialAccess
relevantTechniques:
- T1110
alertRuleTemplateName:
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
customDetails:
entityMappings:
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SourceIp
- entityType: Mailbox
  fieldMappings:
  - identifier: MailboxPrimaryAddress
    columnName: User
- entityType: CloudApplication
  fieldMappings:
  - identifier: AppId
    columnName: Application
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastAudit

Stage 2: where

| where ['Source IP'] !="" and ['Audit Type'] == "Logon Authentication Failed"

Stage 3: extend

| extend   SourceIp = ['Source IP']

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Audit Typeeq
  • Logon Authentication Failed transforms: kusto_literal_column
field:"Audit Type" kind:eq value:"Logon Authentication Failed"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIpextend

Mimecast Targeted Threat Protection - Impersonation Protect

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

'Detects a maliciously tagged impersonation.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: c048fa06-0d50-4626-ae82-a6cea812d9c4
name: Mimecast Targeted Threat Protection - Impersonation Protect
description: |
  'Detects a maliciously tagged impersonation.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MimecastTTPAPI
    dataTypes:
      - MimecastTTPImpersonation
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
- Exfiltration
- Collection
- Discovery
relevantTechniques:
- T1114
query: |
  MimecastTTPImpersonation
  | where ['Tagged Malicious'] == true
  | extend SenderAddress = ['Sender Address'],
    SenderIPAddress = ['Sender IP Address'],
    RecipientAddress = ['Recipient Address']
suppressionDuration: 5h
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P7D
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: MailMessage
    fieldMappings:
      - identifier: Sender
        columnName: SenderAddress
      - identifier: SenderIP
        columnName: SenderIPAddress
      - identifier: Recipient
        columnName: RecipientAddress
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

MimecastTTPImpersonation

Stage 2: where

| where ['Tagged Malicious'] == true

Stage 3: extend

| extend SenderAddress = ['Sender Address'],
  SenderIPAddress = ['Sender IP Address'],
  RecipientAddress = ['Recipient Address']

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Tagged Maliciouseq
  • true transforms: kusto_literal_column
field:"Tagged Malicious" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
RecipientAddressextend
SenderAddressextend
SenderIPAddressextend

No traffic on Sensor Detected (Microsoft Defender for IoT)

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

'This alert leverages Defender for IoT to detect that a sensor can no longer detect the network traffic, which indicates that the system is potentially insecure.'

MITRE ATT&CK Coverage

TacticTechniques
Inhibit Response Function

Rule Body

id: 208c3f5b-3ba2-49b5-9bca-c44e58cd5fd3
name: No traffic on Sensor Detected (Microsoft Defender for IoT)
description: |
  'This alert leverages Defender for IoT to detect that a sensor can no longer detect the network traffic, which indicates that the system is potentially insecure.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InhibitResponseFunction
relevantTechniques:
  - T0881
query: |
  SecurityAlert
  | where ProviderName == "IoTSecurity"
  | where AlertName == "No Traffic Detected on Sensor Interface" 
  | extend ExtendedProperties = parse_json(ExtendedProperties)
  | where tostring(ExtendedProperties.isNew) == "True"
  | extend DeviceId = tostring(ExtendedProperties.DeviceId), 
           SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress), 
           DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress), 
           RemediationSteps = tostring(parse_json(RemediationSteps)[0]), 
           Protocol = tostring(ExtendedProperties.Protocol), 
           AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
  | project
    TimeGenerated,
    DeviceId,
    ProductName,
    ProductComponentName,
    AlertSeverity,
    AlertName,
    Description,
    Protocol,
    SourceDeviceAddress,
    DestDeviceAddress,
    RemediationSteps,
    Tactics,
    Entities,
    VendorOriginalId,
    AlertLink,
    AlertManagementUri,
    Techniques
entityMappings:
sentinelEntitiesMappings:
  - columnName: Entities
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  Sensor: DeviceId
  Protocol: Protocol
  VendorOriginalId: VendorOriginalId
  AlertManagementUri: AlertManagementUri
alertDetailsOverride:
  alertDisplayNameFormat: (MDIoT) {{AlertName}}
  alertDescriptionFormat: (MDIoT) {{Description}}
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: AlertSeverity
  alertDynamicProperties: 
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: RemediationSteps
      value: RemediationSteps
    - alertProperty: Techniques
      value: Techniques
    - alertProperty: ProductComponentName
      value: ProductComponentName
    - alertProperty: AlertLink
      value: AlertLink
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProviderName == "IoTSecurity"

Stage 3: where

| where AlertName == "No Traffic Detected on Sensor Interface"

Stage 4: extend

| extend ExtendedProperties = parse_json(ExtendedProperties)

Stage 5: where

| where tostring(ExtendedProperties.isNew) == "True"

Stage 6: extend

| extend DeviceId = tostring(ExtendedProperties.DeviceId), 
         SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress), 
         DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress), 
         RemediationSteps = tostring(parse_json(RemediationSteps)[0]), 
         Protocol = tostring(ExtendedProperties.Protocol), 
         AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)

Stage 7: project

| project
  TimeGenerated,
  DeviceId,
  ProductName,
  ProductComponentName,
  AlertSeverity,
  AlertName,
  Description,
  Protocol,
  SourceDeviceAddress,
  DestDeviceAddress,
  RemediationSteps,
  Tactics,
  Entities,
  VendorOriginalId,
  AlertLink,
  AlertManagementUri,
  Techniques

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertNameeq
  • No Traffic Detected on Sensor Interface
field:"AlertName" kind:eq value:"No Traffic Detected on Sensor Interface"
ExtendedProperties.isNeweq
  • True transforms: tostring, kusto_dynamic_path corpus 15 (kusto 15)
field:"ExtendedProperties.isNew" kind:eq value:"True"
ProviderNameeq
  • IoTSecurity corpus 15 (kusto 15)
field:"ProviderName" kind:eq value:"IoTSecurity"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertLinkproject
AlertManagementUriproject
AlertNameproject
AlertSeverityproject
Descriptionproject
DestDeviceAddressproject
DeviceIdproject
Entitiesproject
ProductComponentNameproject
ProductNameproject
Protocolproject
RemediationStepsproject
SourceDeviceAddressproject
Tacticsproject
Techniquesproject
TimeGeneratedproject
VendorOriginalIdproject

NRT First access credential added to Application or Service Principal where no credential was present

#
Severity
medium
Author
Samik Roy
Source
github.com/Azure/Azure-Sentinel

This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated. If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential. Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b6988c32-4f3b-4a45-8313-b46b33061a74
name: NRT First access credential added to Application or Service Principal where no credential was present
description: |
  'This will alert when an admin or app owner account adds a new credential to an Application or Service Principal where there was no previous verify KeyCredential associated.
  If a threat actor obtains access to an account with sufficient privileges and adds the alternate authentication material triggering this event, the threat actor can now authenticate as the Application or Service Principal using this credential.
  Additional information on OAuth Credential Grants can be found in RFC 6749 Section 4.4 or https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1550.001
tags:
  - Solorigate
  - NOBELIUM
query: |
  AuditLogs
  | where OperationName has_any ("Add service principal", "Certificates and secrets management")
  | where Result =~ "success"
  | where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"
  | mv-apply TargetResource = TargetResources on 
    (
      where TargetResource.type =~ "Application"
      | extend targetDisplayName = tostring(TargetResource.displayName),
               targetId = tostring(TargetResource.id),
               targetType = tostring(TargetResource.type),
               keyEvents = TargetResource.modifiedProperties
    )
  |  mv-apply Property = keyEvents on 
   (
    where Property.displayName =~ "KeyDescription"
    | extend new_value_set = parse_json(tostring(Property.newValue)),
             old_value_set = parse_json(tostring(Property.oldValue))
   )
  | where old_value_set == "[]"
  | mv-expand new_value_set
  | parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *
  | where keyUsage == "Verify"
   | mv-apply AdditionalDetail = AdditionalDetails on 
    (
      where AdditionalDetail.key =~ "User-Agent"
      | extend UserAgent = tostring(AdditionalDetail.value)
    )
  | 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))
  //| where targetType =~ "Application" // or targetType =~ "ServicePrincipal"
  | project-away new_value_set, old_value_set
  | project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatedByName
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.6
kind: NRT
metadata:
source:
    kind: Community
author:
    name: Samik Roy
support:
    tier: Community
categories:
    domains: [ "Security - Others", "Identity" ]
threatAnalysisTactics: [ "DefenseEvasion" ]
threatAnalysisTechniques: [ "T1550.001" ]

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName has_any ("Add service principal", "Certificates and secrets management")

Stage 3: where

| where Result =~ "success"

Stage 4: where

| where tostring(InitiatedBy.user.userPrincipalName) has "@" or tostring(InitiatedBy.app.displayName) has "@"

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
    where TargetResource.type =~ "Application"
    | extend targetDisplayName = tostring(TargetResource.displayName),
             targetId = tostring(TargetResource.id),
             targetType = tostring(TargetResource.type),
             keyEvents = TargetResource.modifiedProperties
  )

Stage 6: kusto:mv-apply

| mv-apply Property = keyEvents on 
 (
  where Property.displayName =~ "KeyDescription"
  | extend new_value_set = parse_json(tostring(Property.newValue)),
           old_value_set = parse_json(tostring(Property.oldValue))
 )

Stage 7: where

| where old_value_set == "[]"

Stage 8: mv-expand

| mv-expand new_value_set

Stage 9: parse

| parse new_value_set with * "KeyIdentifier=" keyIdentifier:string ",KeyType=" keyType:string ",KeyUsage=" keyUsage:string ",DisplayName=" keyDisplayName:string "]" *

Stage 10: where

| where keyUsage == "Verify"

Stage 11: kusto:mv-apply

| mv-apply AdditionalDetail = AdditionalDetails on 
  (
    where AdditionalDetail.key =~ "User-Agent"
    | extend UserAgent = tostring(AdditionalDetail.value)
  )

Stage 12: extend (5 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))

Stage 13: project-away

| project-away new_value_set, old_value_set

Stage 14: project-reorder

| project-reorder TimeGenerated, OperationName, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, UserAgent, targetDisplayName, targetId, targetType, keyDisplayName, keyType, keyUsage, keyIdentifier, CorrelationId, TenantId

Stage 15: extend

| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AdditionalDetail.keyeq
  • User-Agent transforms: kusto_dynamic_path
field:"AdditionalDetail.key" kind:eq value:"User-Agent"
InitiatedBy.app.displayNamematch
  • @ transforms: tostring, term, kusto_dynamic_path corpus 4 (kusto 4)
field:"InitiatedBy.app.displayName" kind:match value:"@"
InitiatedBy.user.userPrincipalNamematch
  • @ transforms: tostring, term, kusto_dynamic_path corpus 4 (kusto 4)
field:"InitiatedBy.user.userPrincipalName" kind:match value:"@"
OperationNamematch
  • Add service principal transforms: term corpus 4 (kusto 4)
  • Certificates and secrets management transforms: term corpus 5 (kusto 5)
field:"OperationName" kind:match
Property.displayNameeq
  • KeyDescription transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"KeyDescription"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"
TargetResource.typeeq
  • Application transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"Application"
keyUsageeq
  • Verify
field:"keyUsage" kind:eq value:"Verify"
old_value_seteq
  • []
field:"old_value_set" kind:eq value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIpAddressextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

Synqly Alert Event

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

Creates one Microsoft Sentinel alert for each qualifying Synqly-attributed ASIM Alert Event row. Native ASIM values take precedence, with retained OCSF data used to recover investigation context when normalized fields are empty. Replayed source rows can create additional alerts, and Microsoft Sentinel or Defender XDR may correlate related alerts into a shared incident.

Rule Body

id: 3192085a-e97e-440f-acb7-9227622949a4
name: Synqly Alert Event
description: |
  Creates one Microsoft Sentinel alert for each qualifying Synqly-attributed ASIM Alert Event row. Native ASIM values take precedence, with retained OCSF data used to recover investigation context when normalized fields are empty. Replayed source rows can create additional alerts, and Microsoft Sentinel or Defender XDR may correlate related alerts into a shared incident.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SynqlyIntegrationConnector
    dataTypes:
      - ASimAlertEventLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 1h
suppressionEnabled: false
tactics: []
relevantTechniques: []
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5M
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: "{{SynqlyAlertName}}"
  alertDescriptionFormat: "{{SynqlyAlertDescription}}"
  alertSeverityColumnName: SynqlyAlertSeverity
  alertTacticsColumnName: SynqlyTactics
  alertDynamicProperties:
    - alertProperty: Techniques
      value: AllAttackTechniques
    - alertProperty: ConfidenceLevel
      value: SynqlyConfidenceLevel
    - alertProperty: ConfidenceScore
      value: SynqlyConfidenceScore
    - alertProperty: RemediationSteps
      value: SynqlyRemediation
    - alertProperty: AlertLink
      value: SynqlyAlertLink
customDetails:
  SourceAlertId: SynqlyAlertId
  SourceVendor: SourceVendor
  SourceProduct: SourceProduct
  ProductVersion: ProductVersion
  OriginalSeverity: OriginalSeverity
  AttackTechniques: AllAttackTechniques
  SourceRuleId: SourceRuleId
  SourceIp: SourceIp
  DestinationIp: DestinationIp
  DeviceIp: DeviceIp
  PrimaryIpRole: PrimaryIpRole
  DestinationDomain: DestinationDomain
  FilePath: FileValue
  SourceUrl: EntityUrl
  MalwareName: MalwareName
query: |
  ASimAlertEventLogs
  | where tostring(AdditionalFields._ConnectorId) == "SynqlySentinelASIMConnector"
  | extend OCSF = coalesce(todynamic(AdditionalFields.OCSF), dynamic({}))
  | mv-apply Evidence = array_concat(coalesce(todynamic(OCSF.evidences), dynamic([])), dynamic([{}])) on (
      mv-apply EvidenceHash = array_concat(coalesce(todynamic(Evidence.data.file.hashes), dynamic([])), coalesce(todynamic(Evidence.data.process.hashes), dynamic([])), dynamic([{}])) on (
          summarize
              OcsfSHA256 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "SHA256"),
              OcsfSHA1 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "SHA1"),
              OcsfMD5 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "MD5")
      )
      | summarize
          OcsfUserName = take_anyif(tostring(Evidence.data.process.user.name), isnotempty(tostring(Evidence.data.process.user.name))),
          OcsfUserSid = take_anyif(tostring(Evidence.data.process.user.uid), isnotempty(tostring(Evidence.data.process.user.uid))),
          OcsfUserDomain = take_anyif(tostring(Evidence.data.process.user.domain), isnotempty(tostring(Evidence.data.process.user.domain))),
          OcsfProcessId = take_anyif(tostring(Evidence.data.process.pid), isnotempty(tostring(Evidence.data.process.pid))),
          OcsfProcessCommandLine = take_anyif(tostring(Evidence.data.process.cmd_line), isnotempty(tostring(Evidence.data.process.cmd_line))),
          OcsfFileName = take_anyif(tostring(Evidence.data.file.name), isnotempty(tostring(Evidence.data.file.name))),
          OcsfFilePath = take_anyif(tostring(Evidence.data.file.path), isnotempty(tostring(Evidence.data.file.path))),
          OcsfSourceHost = take_anyif(tostring(Evidence.data.network.src_endpoint.host.hostname), isnotempty(tostring(Evidence.data.network.src_endpoint.host.hostname))),
          OcsfSourceIp = take_anyif(tostring(Evidence.data.network.src_endpoint.ip), isnotempty(tostring(Evidence.data.network.src_endpoint.ip))),
          OcsfDestinationIp = take_anyif(tostring(Evidence.data.network.dst_endpoint.ip), isnotempty(tostring(Evidence.data.network.dst_endpoint.ip))),
          OcsfDestinationDomain = take_anyif(tostring(Evidence.data.network.dst_endpoint.domain), isnotempty(tostring(Evidence.data.network.dst_endpoint.domain))),
          OcsfSHA256 = take_anyif(OcsfSHA256, isnotempty(OcsfSHA256)),
          OcsfSHA1 = take_anyif(OcsfSHA1, isnotempty(OcsfSHA1)),
          OcsfMD5 = take_anyif(OcsfMD5, isnotempty(OcsfMD5))
  )
  | mv-apply Attack = array_concat(coalesce(todynamic(OCSF.attacks), dynamic([])), dynamic([{}])) on (
      extend TacticId = tostring(Attack.tactic.uid), TechniqueId = tostring(Attack.technique.uid), SubTechniqueId = tostring(Attack.sub_technique.uid)
      | extend TacticName = case(
          TacticId == "TA0043", "Reconnaissance", TacticId == "TA0042", "ResourceDevelopment",
          TacticId == "TA0001", "InitialAccess", TacticId == "TA0002", "Execution",
          TacticId == "TA0003", "Persistence", TacticId == "TA0004", "PrivilegeEscalation",
          TacticId == "TA0005", "DefenseEvasion", TacticId == "TA0006", "CredentialAccess",
          TacticId == "TA0007", "Discovery", TacticId == "TA0008", "LateralMovement",
          TacticId == "TA0009", "Collection", TacticId == "TA0010", "Exfiltration",
          TacticId == "TA0011", "CommandAndControl", TacticId == "TA0040", "Impact", "")
      | summarize
          OcsfTactics = make_set_if(TacticName, isnotempty(TacticName), 14),
          OcsfTechniques = make_set_if(TechniqueId, isnotempty(TechniqueId), 50),
          OcsfSubTechniques = make_set_if(SubTechniqueId, isnotempty(SubTechniqueId), 50)
  )
  | extend
      ActorUser = todynamic(OCSF.actor.process.user),
      Malware = todynamic(OCSF.malware[0]),
      NativeConfidenceValue = toreal(ThreatConfidence),
      OcsfConfidenceValue = toreal(OCSF.confidence_score),
      NativeConfidenceLabel = tolower(trim(" ", tostring(ThreatOriginalConfidence))),
      OcsfConfidenceLabel = tolower(trim(" ", tostring(OCSF.confidence))),
      NativeSourceId = coalesce(tostring(EventUid), tostring(AlertId), tostring(EventOriginalUid)),
      OcsfSourceId = coalesce(tostring(OCSF.finding_info.uid), tostring(OCSF.uid)),
      SourceIdMaterial = tostring(pack_array(tostring(OCSF.class_uid), tostring(OCSF.type_uid), tostring(OCSF.activity_id), tostring(EventStartTime), tostring(EventEndTime), tostring(EventVendor), tostring(EventProduct), tostring(EventMessage), tostring(OCSF["time"])))
  | extend
      SynqlyAlertId = coalesce(NativeSourceId, OcsfSourceId, strcat("generated-", hash_sha256(SourceIdMaterial))),
      SynqlyAlertName = coalesce(tostring(AlertName), tostring(OCSF.finding_info["title"]), tostring(OCSF.class_name), "Synqly normalized alert event"),
      SynqlyAlertDescription = coalesce(tostring(EventMessage), tostring(AlertDescription), tostring(OCSF.message), "Synqly normalized alert event"),
      SeverityValue = tolower(coalesce(tostring(EventSeverity), tostring(EventOriginalSeverity), tostring(OCSF.severity))),
      SynqlyTactics = coalesce(tostring(AttackTactics), strcat_array(OcsfTactics, ",")),
      AllAttackTechniques = coalesce(tostring(AttackTechniques), strcat_array(array_concat(OcsfTechniques, OcsfSubTechniques), ",")),
      SynqlyConfidenceScore = coalesce(
          iff(NativeConfidenceValue between (0.0 .. 100.0), NativeConfidenceValue / 100.0, real(null)),
          iff(OcsfConfidenceValue between (0.0 .. 100.0), OcsfConfidenceValue / 100.0, real(null))),
      SynqlyConfidenceLevel = case(
          NativeConfidenceLabel == "high", "High",
          NativeConfidenceLabel == "low", "Low",
          NativeConfidenceLabel == "unknown", "Unknown",
          isnotempty(NativeConfidenceLabel), "",
          OcsfConfidenceLabel == "high", "High",
          OcsfConfidenceLabel == "low", "Low",
          OcsfConfidenceLabel == "unknown", "Unknown",
          ""),
      SynqlyRemediation = coalesce(tostring(AttackRemediationSteps), strcat_array(todynamic(OCSF.remediation.kb_articles), "; ")),
      SynqlyAlertLink = coalesce(tostring(EventReportUrl), tostring(OCSF.finding_info.src_url)),
      SourceVendor = coalesce(tostring(EventVendor), tostring(OCSF.metadata.product.vendor_name)),
      SourceProduct = coalesce(tostring(EventProduct), tostring(OCSF.metadata.product.name)),
      ProductVersion = coalesce(tostring(EventProductVersion), tostring(OCSF.metadata.product.version)),
      OriginalSeverity = coalesce(tostring(EventOriginalSeverity), tostring(OCSF.severity)),
      SourceRuleId = coalesce(
          tostring(Rule),
          tostring(OCSF.finding_info.analytic.uid),
          iff(isnotempty(tostring(RuleName)) and not(tostring(RuleName) matches regex @"^\d+$"), tostring(RuleName), "")),
      AccountName = coalesce(tostring(Username), tostring(User), tostring(ActorUser.name), OcsfUserName),
      AccountSid = coalesce(iff(tolower(tostring(UserIdType)) contains "sid", tostring(UserId), ""), tostring(ActorUser.uid), OcsfUserSid),
      AccountDomain = coalesce(tostring(UserScope), tostring(ActorUser.domain), OcsfUserDomain),
      NativeDeviceHost = coalesce(tostring(DvcHostname), tostring(DvcFQDN)),
      NativeDeviceHostDomain = coalesce(tostring(DvcDomain), extract(@"^[^.]+\.(.+)$", 1, tostring(DvcFQDN)), extract(@"^[^.]+\.(.+)$", 1, tostring(DvcHostname))),
      DeviceIp = tostring(DvcIpAddr),
      DestinationDomain = OcsfDestinationDomain,
      EntityUrl = coalesce(tostring(Url), tostring(OCSF.finding_info.src_url)),
      FileValue = coalesce(tostring(FilePath), OcsfFilePath),
      ProcessIdValue = coalesce(tostring(ProcessId), OcsfProcessId),
      ProcessCommandLineValue = coalesce(tostring(ProcessCommandLine), OcsfProcessCommandLine),
      MalwareName = coalesce(tostring(ThreatName), tostring(Malware.name)),
      FileHashValue = coalesce(tostring(FileSHA256), OcsfSHA256, tostring(FileSHA1), OcsfSHA1, tostring(FileMD5), OcsfMD5)
  | extend
      SourceIp = OcsfSourceIp,
      DestinationIp = OcsfDestinationIp,
      HostValue = coalesce(NativeDeviceHost, OcsfSourceHost),
      HostDomainValue = iff(isnotempty(NativeDeviceHost), NativeDeviceHostDomain, extract(@"^[^.]+\.(.+)$", 1, OcsfSourceHost))
  | extend
      PrimaryIp = coalesce(SourceIp, DestinationIp, tostring(DvcIpAddr)),
      PrimaryIpRole = case(isnotempty(SourceIp), "Source", isnotempty(DestinationIp), "Destination", isnotempty(tostring(DvcIpAddr)), "Device", "")
  | extend
      SynqlyAlertSeverity = case(
          SeverityValue == "informational", "Informational",
          SeverityValue == "low", "Low",
          SeverityValue == "medium", "Medium",
          SeverityValue in ("high", "critical", "fatal"), "High",
          "Medium"),
      HostNameValue = coalesce(extract(@"^([^.]+)", 1, HostValue), HostValue),
      FileHashAlgorithm = case(
          isnotempty(FileSHA256) or isnotempty(OcsfSHA256), "SHA256",
          isnotempty(FileSHA1) or isnotempty(OcsfSHA1), "SHA1",
          isnotempty(FileMD5) or isnotempty(OcsfMD5), "MD5",
          "")
  | project
      TimeGenerated,
      SynqlyAlertId,
      SynqlyAlertName,
      SynqlyAlertDescription,
      SynqlyAlertSeverity,
      SynqlyTactics,
      AllAttackTechniques,
      SynqlyConfidenceLevel,
      SynqlyConfidenceScore,
      SynqlyRemediation,
      SynqlyAlertLink,
      SourceVendor,
      SourceProduct,
      ProductVersion,
      OriginalSeverity,
      SourceRuleId,
      AccountName,
      AccountSid,
      AccountDomain,
      HostNameValue,
      HostDomainValue,
      SourceIp,
      DestinationIp,
      DeviceIp,
      PrimaryIp,
      PrimaryIpRole,
      DestinationDomain,
      EntityUrl,
      FileValue,
      FileHashAlgorithm,
      FileHashValue,
      ProcessIdValue,
      ProcessCommandLineValue,
      MalwareName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: Sid
        columnName: AccountSid
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostNameValue
      - identifier: DnsDomain
        columnName: HostDomainValue
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: PrimaryIp
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: FileHashAlgorithm
      - identifier: Value
        columnName: FileHashValue
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ProcessIdValue
      - identifier: CommandLine
        columnName: ProcessCommandLineValue
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

ASimAlertEventLogs

Stage 2: where

| where tostring(AdditionalFields._ConnectorId) == "SynqlySentinelASIMConnector"

Stage 3: extend

| extend OCSF = coalesce(todynamic(AdditionalFields.OCSF), dynamic({}))

Stage 4: kusto:mv-apply

| mv-apply Evidence = array_concat(coalesce(todynamic(OCSF.evidences), dynamic([])), dynamic([{}])) on (
    mv-apply EvidenceHash = array_concat(coalesce(todynamic(Evidence.data.file.hashes), dynamic([])), coalesce(todynamic(Evidence.data.process.hashes), dynamic([])), dynamic([{}])) on (
        summarize
            OcsfSHA256 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "SHA256"),
            OcsfSHA1 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "SHA1"),
            OcsfMD5 = take_anyif(tostring(EvidenceHash.value), toupper(tostring(EvidenceHash.algorithm)) == "MD5")
    )
    | summarize
        OcsfUserName = take_anyif(tostring(Evidence.data.process.user.name), isnotempty(tostring(Evidence.data.process.user.name))),
        OcsfUserSid = take_anyif(tostring(Evidence.data.process.user.uid), isnotempty(tostring(Evidence.data.process.user.uid))),
        OcsfUserDomain = take_anyif(tostring(Evidence.data.process.user.domain), isnotempty(tostring(Evidence.data.process.user.domain))),
        OcsfProcessId = take_anyif(tostring(Evidence.data.process.pid), isnotempty(tostring(Evidence.data.process.pid))),
        OcsfProcessCommandLine = take_anyif(tostring(Evidence.data.process.cmd_line), isnotempty(tostring(Evidence.data.process.cmd_line))),
        OcsfFileName = take_anyif(tostring(Evidence.data.file.name), isnotempty(tostring(Evidence.data.file.name))),
        OcsfFilePath = take_anyif(tostring(Evidence.data.file.path), isnotempty(tostring(Evidence.data.file.path))),
        OcsfSourceHost = take_anyif(tostring(Evidence.data.network.src_endpoint.host.hostname), isnotempty(tostring(Evidence.data.network.src_endpoint.host.hostname))),
        OcsfSourceIp = take_anyif(tostring(Evidence.data.network.src_endpoint.ip), isnotempty(tostring(Evidence.data.network.src_endpoint.ip))),
        OcsfDestinationIp = take_anyif(tostring(Evidence.data.network.dst_endpoint.ip), isnotempty(tostring(Evidence.data.network.dst_endpoint.ip))),
        OcsfDestinationDomain = take_anyif(tostring(Evidence.data.network.dst_endpoint.domain), isnotempty(tostring(Evidence.data.network.dst_endpoint.domain))),
        OcsfSHA256 = take_anyif(OcsfSHA256, isnotempty(OcsfSHA256)),
        OcsfSHA1 = take_anyif(OcsfSHA1, isnotempty(OcsfSHA1)),
        OcsfMD5 = take_anyif(OcsfMD5, isnotempty(OcsfMD5))
)

Stage 5: kusto:mv-apply

| mv-apply Attack = array_concat(coalesce(todynamic(OCSF.attacks), dynamic([])), dynamic([{}])) on (
    extend TacticId = tostring(Attack.tactic.uid), TechniqueId = tostring(Attack.technique.uid), SubTechniqueId = tostring(Attack.sub_technique.uid)
    | extend TacticName = case(
        TacticId == "TA0043", "Reconnaissance", TacticId == "TA0042", "ResourceDevelopment",
        TacticId == "TA0001", "InitialAccess", TacticId == "TA0002", "Execution",
        TacticId == "TA0003", "Persistence", TacticId == "TA0004", "PrivilegeEscalation",
        TacticId == "TA0005", "DefenseEvasion", TacticId == "TA0006", "CredentialAccess",
        TacticId == "TA0007", "Discovery", TacticId == "TA0008", "LateralMovement",
        TacticId == "TA0009", "Collection", TacticId == "TA0010", "Exfiltration",
        TacticId == "TA0011", "CommandAndControl", TacticId == "TA0040", "Impact", "")
    | summarize
        OcsfTactics = make_set_if(TacticName, isnotempty(TacticName), 14),
        OcsfTechniques = make_set_if(TechniqueId, isnotempty(TechniqueId), 50),
        OcsfSubTechniques = make_set_if(SubTechniqueId, isnotempty(SubTechniqueId), 50)
)

Stage 6: extend (5 consecutive steps)

| extend
    ActorUser = todynamic(OCSF.actor.process.user),
    Malware = todynamic(OCSF.malware[0]),
    NativeConfidenceValue = toreal(ThreatConfidence),
    OcsfConfidenceValue = toreal(OCSF.confidence_score),
    NativeConfidenceLabel = tolower(trim(" ", tostring(ThreatOriginalConfidence))),
    OcsfConfidenceLabel = tolower(trim(" ", tostring(OCSF.confidence))),
    NativeSourceId = coalesce(tostring(EventUid), tostring(AlertId), tostring(EventOriginalUid)),
    OcsfSourceId = coalesce(tostring(OCSF.finding_info.uid), tostring(OCSF.uid)),
    SourceIdMaterial = tostring(pack_array(tostring(OCSF.class_uid), tostring(OCSF.type_uid), tostring(OCSF.activity_id), tostring(EventStartTime), tostring(EventEndTime), tostring(EventVendor), tostring(EventProduct), tostring(EventMessage), tostring(OCSF["time"])))
| extend
    SynqlyAlertId = coalesce(NativeSourceId, OcsfSourceId, strcat("generated-", hash_sha256(SourceIdMaterial))),
    SynqlyAlertName = coalesce(tostring(AlertName), tostring(OCSF.finding_info["title"]), tostring(OCSF.class_name), "Synqly normalized alert event"),
    SynqlyAlertDescription = coalesce(tostring(EventMessage), tostring(AlertDescription), tostring(OCSF.message), "Synqly normalized alert event"),
    SeverityValue = tolower(coalesce(tostring(EventSeverity), tostring(EventOriginalSeverity), tostring(OCSF.severity))),
    SynqlyTactics = coalesce(tostring(AttackTactics), strcat_array(OcsfTactics, ",")),
    AllAttackTechniques = coalesce(tostring(AttackTechniques), strcat_array(array_concat(OcsfTechniques, OcsfSubTechniques), ",")),
    SynqlyConfidenceScore = coalesce(
        iff(NativeConfidenceValue between (0.0 .. 100.0), NativeConfidenceValue / 100.0, real(null)),
        iff(OcsfConfidenceValue between (0.0 .. 100.0), OcsfConfidenceValue / 100.0, real(null))),
    SynqlyConfidenceLevel = case(
        NativeConfidenceLabel == "high", "High",
        NativeConfidenceLabel == "low", "Low",
        NativeConfidenceLabel == "unknown", "Unknown",
        isnotempty(NativeConfidenceLabel), "",
        OcsfConfidenceLabel == "high", "High",
        OcsfConfidenceLabel == "low", "Low",
        OcsfConfidenceLabel == "unknown", "Unknown",
        ""),
    SynqlyRemediation = coalesce(tostring(AttackRemediationSteps), strcat_array(todynamic(OCSF.remediation.kb_articles), "; ")),
    SynqlyAlertLink = coalesce(tostring(EventReportUrl), tostring(OCSF.finding_info.src_url)),
    SourceVendor = coalesce(tostring(EventVendor), tostring(OCSF.metadata.product.vendor_name)),
    SourceProduct = coalesce(tostring(EventProduct), tostring(OCSF.metadata.product.name)),
    ProductVersion = coalesce(tostring(EventProductVersion), tostring(OCSF.metadata.product.version)),
    OriginalSeverity = coalesce(tostring(EventOriginalSeverity), tostring(OCSF.severity)),
    SourceRuleId = coalesce(
        tostring(Rule),
        tostring(OCSF.finding_info.analytic.uid),
        iff(isnotempty(tostring(RuleName)) and not(tostring(RuleName) matches regex @"^\d+$"), tostring(RuleName), "")),
    AccountName = coalesce(tostring(Username), tostring(User), tostring(ActorUser.name), OcsfUserName),
    AccountSid = coalesce(iff(tolower(tostring(UserIdType)) contains "sid", tostring(UserId), ""), tostring(ActorUser.uid), OcsfUserSid),
    AccountDomain = coalesce(tostring(UserScope), tostring(ActorUser.domain), OcsfUserDomain),
    NativeDeviceHost = coalesce(tostring(DvcHostname), tostring(DvcFQDN)),
    NativeDeviceHostDomain = coalesce(tostring(DvcDomain), extract(@"^[^.]+\.(.+)$", 1, tostring(DvcFQDN)), extract(@"^[^.]+\.(.+)$", 1, tostring(DvcHostname))),
    DeviceIp = tostring(DvcIpAddr),
    DestinationDomain = OcsfDestinationDomain,
    EntityUrl = coalesce(tostring(Url), tostring(OCSF.finding_info.src_url)),
    FileValue = coalesce(tostring(FilePath), OcsfFilePath),
    ProcessIdValue = coalesce(tostring(ProcessId), OcsfProcessId),
    ProcessCommandLineValue = coalesce(tostring(ProcessCommandLine), OcsfProcessCommandLine),
    MalwareName = coalesce(tostring(ThreatName), tostring(Malware.name)),
    FileHashValue = coalesce(tostring(FileSHA256), OcsfSHA256, tostring(FileSHA1), OcsfSHA1, tostring(FileMD5), OcsfMD5)
| extend
    SourceIp = OcsfSourceIp,
    DestinationIp = OcsfDestinationIp,
    HostValue = coalesce(NativeDeviceHost, OcsfSourceHost),
    HostDomainValue = iff(isnotempty(NativeDeviceHost), NativeDeviceHostDomain, extract(@"^[^.]+\.(.+)$", 1, OcsfSourceHost))
| extend
    PrimaryIp = coalesce(SourceIp, DestinationIp, tostring(DvcIpAddr)),
    PrimaryIpRole = case(isnotempty(SourceIp), "Source", isnotempty(DestinationIp), "Destination", isnotempty(tostring(DvcIpAddr)), "Device", "")
| extend
    SynqlyAlertSeverity = case(
        SeverityValue == "informational", "Informational",
        SeverityValue == "low", "Low",
        SeverityValue == "medium", "Medium",
        SeverityValue in ("high", "critical", "fatal"), "High",
        "Medium"),
    HostNameValue = coalesce(extract(@"^([^.]+)", 1, HostValue), HostValue),
    FileHashAlgorithm = case(
        isnotempty(FileSHA256) or isnotempty(OcsfSHA256), "SHA256",
        isnotempty(FileSHA1) or isnotempty(OcsfSHA1), "SHA1",
        isnotempty(FileMD5) or isnotempty(OcsfMD5), "MD5",
        "")

Stage 7: project

| project
    TimeGenerated,
    SynqlyAlertId,
    SynqlyAlertName,
    SynqlyAlertDescription,
    SynqlyAlertSeverity,
    SynqlyTactics,
    AllAttackTechniques,
    SynqlyConfidenceLevel,
    SynqlyConfidenceScore,
    SynqlyRemediation,
    SynqlyAlertLink,
    SourceVendor,
    SourceProduct,
    ProductVersion,
    OriginalSeverity,
    SourceRuleId,
    AccountName,
    AccountSid,
    AccountDomain,
    HostNameValue,
    HostDomainValue,
    SourceIp,
    DestinationIp,
    DeviceIp,
    PrimaryIp,
    PrimaryIpRole,
    DestinationDomain,
    EntityUrl,
    FileValue,
    FileHashAlgorithm,
    FileHashValue,
    ProcessIdValue,
    ProcessCommandLineValue,
    MalwareName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AdditionalFields._ConnectorIdeq
  • SynqlySentinelASIMConnector transforms: tostring, kusto_dynamic_path
field:"AdditionalFields._ConnectorId" kind:eq value:"SynqlySentinelASIMConnector"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDomainproject
AccountNameproject
AccountSidproject
AllAttackTechniquesproject
DestinationDomainproject
DestinationIpproject
DeviceIpproject
EntityUrlproject
FileHashAlgorithmproject
FileHashValueproject
FileValueproject
HostDomainValueproject
HostNameValueproject
MalwareNameproject
OriginalSeverityproject
PrimaryIpproject
PrimaryIpRoleproject
ProcessCommandLineValueproject
ProcessIdValueproject
ProductVersionproject
SourceIpproject
SourceProductproject
SourceRuleIdproject
SourceVendorproject
SynqlyAlertDescriptionproject
SynqlyAlertIdproject
SynqlyAlertLinkproject
SynqlyAlertNameproject
SynqlyAlertSeverityproject
SynqlyConfidenceLevelproject
SynqlyConfidenceScoreproject
SynqlyRemediationproject
SynqlyTacticsproject
TimeGeneratedproject

Tailscale: Unauthorized device connected to control plane

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

Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). Often benign onboarding but can indicate rogue joins.

MITRE ATT&CK Coverage

Rule Body

id: a1b2c3d4-5678-9012-3456-789012340042
name: "Tailscale: Unauthorized device connected to control plane"
description: Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). Often benign onboarding but can indicate rogue joins.
description-detailed: |
  Identifies devices actively connected to the Tailscale control plane (ConnectedToControl=true) but not yet authorized by an admin (Authorized=false). With device approval enabled, persistent entries warrant review; without approval, persistence may indicate a node-key injection attempt.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Persistence
relevantTechniques:
  - T1078
  - T1098
query: |
  Tailscale_Devices_CL
  | where TimeGenerated > ago(1h)
  | summarize arg_max(TimeGenerated, *) by DeviceId
  | where Authorized == false and ConnectedToControl == true
  | project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, MachineKey, NodeKey
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where Authorized == false and ConnectedToControl == true

Stage 5: project

| project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, Created, LastSeen, MachineKey, NodeKey

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientVersionproject
Createdproject
DeviceNameproject
Hostnameproject
LastSeenproject
MachineKeyproject
NodeKeyproject
Osproject
TimeGeneratedproject
Userproject

Uniqkey - Departing employee credential export

#
Severity
high
Time window
14d
Group by
$left.RemovedEmployeeId, $right.ActorId
Source
github.com/Azure/Azure-Sentinel

Correlates the archival, deletion or detachment of a Uniqkey employee account with data exports performed by that same employee during the preceding 14 days. An employee who exports credentials shortly before losing access is a classic insider-risk scenario, and organization credentials touched by the export should be considered for rotation. The action filter matches the offboarding action identifiers from the Uniqkey audit-log catalog, with a name-based fallback for future action variants.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: cee6bbae-e1ee-4b65-9782-6afce5c330e6
name: Uniqkey - Departing employee credential export
version: 1.0.0
kind: Scheduled
description: |-
  Correlates the archival, deletion or detachment of a Uniqkey employee account with data exports performed by that same employee during the preceding 14 days. An employee who exports credentials shortly before losing access is a classic insider-risk scenario, and organization credentials touched by the export should be considered for rotation. The action filter matches the offboarding action identifiers from the Uniqkey audit-log catalog, with a name-based fallback for future action variants.
severity: High
requiredDataConnectors:
  - connectorId: UniqkeyEventsConnector
    dataTypes:
      - UniqkeyEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- Exfiltration
relevantTechniques:
- T1555
query: |-
  let lookback = 14d;
  let frequency = 1h;
  let exports =
      UniqkeyEvents_CL
      | where TimeGenerated > ago(lookback)
      | where Category == "data_export"
      | project ExportTime = TimeGenerated, ActorId, ActorEmail, ExportAction = Action, ExportedTargetType = TargetType, ExportedTargetName = TargetName, SrcIpAddr;
  let removalActionIds = dynamic([
      "a2e18fcb-4d28-4f53-b2e3-24f03d0e188e", // Archive employee account
      "319c8a05-abd5-4b13-b51f-d6198578e61c", // Archive bulk employee
      "f1ed588f-975b-4bbe-83e9-e7a5c23e647f", // Hard delete employee
      "f8b81b6c-0082-4ed8-916e-83350f932e89", // Permanent delete employee account
      "4361dd86-0f70-47e0-91de-071a81d49834"  // Detach bulk employee account
  ]);
  UniqkeyEvents_CL
  | where TimeGenerated > ago(frequency)
  | where Category == "account_management"
  | where tolower(ActionId) in (removalActionIds) or Action has_any ("archive", "delete", "detach")
  | where TargetType == "employee"
  | project RemovalTime = TimeGenerated, RemovalAction = Action, RemovedEmployeeId = TargetId, RemovedEmployeeName = TargetName, RemovalActor = ActorEmail
  | join kind=inner exports on $left.RemovedEmployeeId == $right.ActorId
  | project RemovalTime, RemovalAction, RemovedEmployeeName, RemovalActor, ExportTime, ExportAction, ExportedTargetType, ExportedTargetName, ActorEmail, SrcIpAddr
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorEmail
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let lookback = 14d;
let frequency = 1h;

Let Binding: exports used in Stage 7

let exports = UniqkeyEvents_CL
    | where TimeGenerated > ago(lookback)
    | where Category == "data_export"
    | project ExportTime = TimeGenerated, ActorId, ActorEmail, ExportAction = Action, ExportedTargetType = TargetType, ExportedTargetName = TargetName, SrcIpAddr;

Let Binding: removalActionIds used in Stage 4

let removalActionIds = dynamic([
    "a2e18fcb-4d28-4f53-b2e3-24f03d0e188e",
    "319c8a05-abd5-4b13-b51f-d6198578e61c",
    "f1ed588f-975b-4bbe-83e9-e7a5c23e647f",
    "f8b81b6c-0082-4ed8-916e-83350f932e89",
    "4361dd86-0f70-47e0-91de-071a81d49834"
]);

Stage 1: source

UniqkeyEvents_CL

Stage 2: where

| where TimeGenerated > ago(frequency)

Stage 3: where

| where Category == "account_management"

Stage 4: where

| where tolower(ActionId) in (removalActionIds) or Action has_any ("archive", "delete", "detach")

Stage 5: where

| where TargetType == "employee"

Stage 6: project

| project RemovalTime = TimeGenerated, RemovalAction = Action, RemovedEmployeeId = TargetId, RemovedEmployeeName = TargetName, RemovalActor = ActorEmail

Stage 7: join

| join kind=inner exports on $left.RemovedEmployeeId == $right.ActorId

Stage 8: project

project ActorEmail, ExportAction, ExportTime, ExportedTargetName, ExportedTargetType, RemovalAction, RemovalActor, RemovalTime, RemovedEmployeeName, SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionmatch
  • archive transforms: term
  • delete transforms: term
  • detach transforms: term
field:"Action" kind:match
ActionIdin
  • 319c8a05-abd5-4b13-b51f-d6198578e61c transforms: tolower
  • 4361dd86-0f70-47e0-91de-071a81d49834 transforms: tolower
  • a2e18fcb-4d28-4f53-b2e3-24f03d0e188e transforms: tolower
  • f1ed588f-975b-4bbe-83e9-e7a5c23e647f transforms: tolower
  • f8b81b6c-0082-4ed8-916e-83350f932e89 transforms: tolower
field:"ActionId" kind:in
Categoryeq
  • account_management
  • data_export
field:"Category" kind:eq
TargetTypeeq
  • employee
field:"TargetType" kind:eq value:"employee"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorEmailproject
ExportActionproject
ExportTimeproject
ExportedTargetNameproject
ExportedTargetTypeproject
RemovalActionproject
RemovalActorproject
RemovalTimeproject
RemovedEmployeeNameproject
SrcIpAddrproject

Back to all Kusto rules