Detection rules › Kusto

Kusto rules: 1w

Abnormal Security - User-reported email judged malicious

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

'Creates an incident when an email a user reported to the Abnormal Security Abuse Mailbox is judged malicious. Surfaces the reporter, the original recipient, and the campaign so analysts can scope and remediate look-alike messages. Spam- and safe-judged reports are intentionally excluded.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 51c6ba55-fecd-4be0-9064-1aafc4d3e8d6
name: Abnormal Security - User-reported email judged malicious
description: |
  'Creates an incident when an email a user reported to the Abnormal Security Abuse
  Mailbox is judged malicious. Surfaces the reporter, the original recipient, and the
  campaign so analysts can scope and remediate look-alike messages. Spam- and safe-judged
  reports are intentionally excluded.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AbnormalSecurityPush
    dataTypes:
      - ABNORMAL_SECURITY_ABUSE_MAILBOX_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  ABNORMAL_SECURITY_ABUSE_MAILBOX_CL
  | where abx_body_abx_body_reported_b == true
  | where tolower(abx_body_abx_body_judgement_s) == "malicious"
  | extend
      ReporterEmail = abx_body_abx_body_reporter_address_s,
      RecipientEmail = abx_body_abx_body_recipient_address_s,
      Subject = abx_body_abx_body_subject_s,
      Judgement = abx_body_abx_body_judgement_s,
      MessageId = abx_body_abx_body_internet_message_id_s,
      CampaignId = abx_body_abx_body_campaign_id_g
  | project TimeGenerated, ReporterEmail, RecipientEmail, Subject, Judgement, MessageId, CampaignId
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ReporterEmail
  - entityType: MailMessage
    fieldMappings:
      - identifier: Recipient
        columnName: RecipientEmail
      - identifier: Subject
        columnName: Subject
      - identifier: NetworkMessageId
        columnName: MessageId
customDetails:
  Judgement: Judgement
  CampaignId: CampaignId
alertDetailsOverride:
  alertDisplayNameFormat: "Abnormal Security: user-reported {{Judgement}} email ({{Subject}})"
  alertDescriptionFormat: "An email reported by {{ReporterEmail}} was judged {{Judgement}} by Abnormal Security. Original recipient: {{RecipientEmail}}."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

ABNORMAL_SECURITY_ABUSE_MAILBOX_CL

Stage 2: where

| where abx_body_abx_body_reported_b == true

Stage 3: where

| where tolower(abx_body_abx_body_judgement_s) == "malicious"

Stage 4: extend

| extend
    ReporterEmail = abx_body_abx_body_reporter_address_s,
    RecipientEmail = abx_body_abx_body_recipient_address_s,
    Subject = abx_body_abx_body_subject_s,
    Judgement = abx_body_abx_body_judgement_s,
    MessageId = abx_body_abx_body_internet_message_id_s,
    CampaignId = abx_body_abx_body_campaign_id_g

Stage 5: project

| project TimeGenerated, ReporterEmail, RecipientEmail, Subject, Judgement, MessageId, CampaignId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CampaignIdproject
Judgementproject
MessageIdproject
RecipientEmailproject
ReporterEmailproject
Subjectproject
TimeGeneratedproject

ApexOne - Suspicious commandline arguments

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

'Detects suspicious commandline arguments.'

MITRE ATT&CK Coverage

Rule Body

id: 4d7199b2-67b8-11ec-90d6-0242ac120003
name: ApexOne - Suspicious commandline arguments
description: |
  'Detects suspicious commandline arguments.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  TMApexOneEvent
  | where EventMessage has "Endpoint Application"
  | where Command has_any ("whoami", "dpkg", "useradd", "sudo")
  | 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 EventMessage has "Endpoint Application"

Stage 3: where

| where Command has_any ("whoami", "dpkg", "useradd", "sudo")

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr, AccountCustomEntity = DstUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Commandmatch
  • dpkg transforms: term
  • sudo transforms: term
  • useradd transforms: term
  • whoami transforms: term
field:"Command" kind:match
EventMessagematch
  • Endpoint Application transforms: term
field:"EventMessage" kind:match value:"Endpoint Application"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

Azure secure score MFA registration V2

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

'This query searches for multi-factor authentication (MFA) helps protect devices and data that are accessible to these users. Adding more authentication methods, such as the Microsoft Authenticator app or a phone number, increases the level of protection if one factor is compromised.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 8EB2B20A-BF64-4DCC-9D98-1AD559502C00
name: Azure secure score MFA registration V2
description: |
  'This query searches for multi-factor authentication (MFA) helps protect devices and data that are accessible to these users. Adding more authentication methods, such as the Microsoft Authenticator app or a phone number, increases the level of protection if one factor is compromised.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SenservaPro
    dataTypes:
      - SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1056
query: |
  SenservaPro_CL
  | where ControlName_s == 'AzureSecureScoreMFARegistrationV2'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ControlName_s
      - identifier: AadTenantId
        columnName: TenantId
      - identifier: DisplayName
        columnName: TenantDisplayName_s 
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: Group_s
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourceSystem 
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

SenservaPro_CL

Stage 2: where

| where ControlName_s == 'AzureSecureScoreMFARegistrationV2'

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ControlName_seq
  • AzureSecureScoreMFARegistrationV2
field:"ControlName_s" kind:eq value:"AzureSecureScoreMFARegistrationV2"

Cisco SE - Possible webshell

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

'Detects possible webshell on host.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: d2c97cc9-1ccc-494d-bad4-564700451a2b
name: Cisco SE - Possible webshell
description: |
  'Detects possible webshell on host.'
severity: High
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CommandAndControl
relevantTechniques:
  - T1102
query: |
  CiscoSecureEndpoint
  | where EventMessage has 'Possible webshell'
  | extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSecureEndpoint

Stage 2: where

| where EventMessage has 'Possible webshell'

Stage 3: extend

| extend HostCustomEntity = DstHostname, MalwareCustomEntity = ThreatName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventMessagematch
  • Possible webshell transforms: term
field:"EventMessage" kind:match value:"Possible webshell"

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
MalwareCustomEntityextend

CTERA Mass Access Denied Detection Analytic

#
Status
available
Severity
high
Time window
5m
Group by
EdgeFiler, UserName
Source
github.com/Azure/Azure-Sentinel

This analytic rule detects and alerts when access denied operations generated by the CTERA Edge Filer goes over a predefined threshold

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 88341fc3-38e1-46db-8bb1-6c052e749991
name: CTERA Mass Access Denied Detection Analytic
description: 'This analytic rule detects and alerts when access denied operations generated by the CTERA Edge Filer goes over a predefined threshold'
kind: Scheduled
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CTERA
    dataTypes:
      - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 1000
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  Syslog
  | where ProcessName == 'gw-audit'
  | extend
      TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
      UserName = extract("user=([^|]*)", 1, SyslogMessage),
      Operation = extract("op=([^|]*)", 1, SyslogMessage),
      EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
      RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
      Share = extract("share=([^|]*)", 1, SyslogMessage),
      LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
      Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))
  | where Operation in ('OpenDenied', 'createDenied', 'AclDenied', 'deleteDenied')
  | summarize Count = count() by bin(Timestamp, 5m), UserName, EdgeFiler
  | where Count > 1000
incidentConfiguration:
    createIncident: true
    groupingConfiguration:
        enabled: false
        reopenClosedIncident: false
        lookbackDuration: PT5H
        matchingMethod: AllEntities
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride:
    alertnameFormat: 'CTERA Batch Access Denied Detection'
    alertDescriptionFormat: >
      Detected {{Count}} denied access attempts by user {{UserName}} on Edge Filer 
      {{EdgeFiler}} within 5 minutes. Please investigate unauthorized access attempts or misconfigurations.
customDetails:
    TenantName: TenantName
    UserName: UserName
    EdgeFiler: EdgeFiler
    RootPath: RootPath
    Share: Share
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: UserName
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: EdgeFiler
suppressionDuration: PT5H
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Syslog

Stage 2: where

| where ProcessName == 'gw-audit'

Stage 3: extend

| extend
    TenantName = extract("\"vportal\":\"([^\"]*)\"", 1, SyslogMessage),
    UserName = extract("user=([^|]*)", 1, SyslogMessage),
    Operation = extract("op=([^|]*)", 1, SyslogMessage),
    EdgeFiler = extract("\"client\":\"([^\"]*)\"", 1, SyslogMessage),
    RootPath = extract("rootPath=([^|]*)", 1, SyslogMessage),
    Share = extract("share=([^|]*)", 1, SyslogMessage),
    LocalPath = extract("path=([^|]*)", 1, SyslogMessage),
    Timestamp = todatetime(extract("\"@timestamp\":\"([^\"]*)\"", 1, SyslogMessage))

Stage 4: where

| where Operation in ('OpenDenied', 'createDenied', 'AclDenied', 'deleteDenied')

Stage 5: summarize

| summarize Count = count() by bin(Timestamp, 5m), UserName, EdgeFiler

Stage 6: where

| where Count > 1000

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Countgt
  • 1000 transforms: number
field:"Count" kind:gt value:"1000"
Operationin
  • AclDenied
  • OpenDenied
  • createDenied
  • deleteDenied
field:"Operation" kind:in
ProcessNameeq
  • gw-audit
field:"process_name" kind:eq value:"gw-audit"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countsummarize
EdgeFilersummarize
UserNamesummarize

CYFIRMA - High severity File Hash Indicators with Block Action and Malware

#

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.

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

"This KQL query retrieves file hash indicators (MD5, SHA1, SHA256) from the CyfirmaIndicators_CL table within the last 5 minutes. It filters records with a confidence score of 80 or higher, containing file hash patterns, a recommended action of 'Block', and roles marked as 'Malware'. Extracted hashes and key threat intelligence details are projected for Blocking and investigation."

MITRE ATT&CK Coverage

Rule Body

id: 990fc0dc-e7a5-4f6d-bc24-8569652cd773
name: CYFIRMA - High severity File Hash Indicators with Block Action and Malware
description: |
  "This KQL query retrieves file hash indicators (MD5, SHA1, SHA256) from the CyfirmaIndicators_CL table within the last 5 minutes. 
  It filters records with a confidence score of 80 or higher, containing file hash patterns, a recommended action of 'Block', and roles marked as 'Malware'. 
  Extracted hashes and key threat intelligence details are projected for Blocking and investigation."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // File Hash Indicators with Block Action and Malware
  let timeFrame = 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains "Malware")
  | extend MD5 = extract(@"file:hashes\.md5\s*=\s*'([a-fA-F0-9]{32})'", 1, pattern)
  | extend SHA1 = extract(@"file:hashes\.'SHA-1'\s*=\s*'([a-fA-F0-9]{40})'", 1, pattern)
  | extend SHA256 = extract(@"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'", 1, pattern)
  | extend
      Algo_MD5='md5',
      Algo_SHA1= 'SHA1',
      Algo_SHA256='SHA256',
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project  
      MD5,
      Algo_MD5,
      SHA1,
      Algo_SHA1,
      SHA256,
      Algo_SHA256,
      ThreatActors,
      Sources,
      RecommendedActions,
      Roles,
      Country,
      name,
      Description,
      ConfidenceScore,
      SecurityVendors,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - InitialAccess
  - Execution
  - Persistence
  - PrivilegeEscalation
  - DefenseEvasion
  - CredentialAccess
  - Discovery
  - LateralMovement
  - Collection
  - Impact
relevantTechniques:
  - T1566
  - T1203
  - T1059
  - T1204
  - T1547
  - T1053
  - T1055
  - T1027
  - T1562
  - T1036
  - T1003
  - T1555
  - T1082
  - T1057
  - T1021
  - T1113
  - T1486
  - T1566.001
  - T1059.001
  - T1059.003
  - T1547.001
  - T1053.005
  - T1562.001
  - T1003.001
  - T1555.003
  - T1021.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence File Hash Indicators with Block Action and Malware - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProviderName
      value: ProviderName
    - alertProperty: ProductName
      value: ProductName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  created: created
  modified: modified
  valid_from: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
entityMappings:
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_MD5
      - identifier: Value
        columnName: MD5
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA1
      - identifier: Value
        columnName: SHA1
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA256
      - identifier: Value
        columnName: SHA256
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

CyfirmaIndicators_CL

Stage 2: where

| where ConfidenceScore >= 80
    and TimeGenerated between (ago(timeFrame) .. now())
    and pattern contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains "Malware")

Stage 3: extend (4 consecutive steps)

| extend MD5 = extract(@"file:hashes\.md5\s*=\s*'([a-fA-F0-9]{32})'", 1, pattern)
| extend SHA1 = extract(@"file:hashes\.'SHA-1'\s*=\s*'([a-fA-F0-9]{40})'", 1, pattern)
| extend SHA256 = extract(@"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'", 1, pattern)
| extend
    Algo_MD5='md5',
    Algo_SHA1= 'SHA1',
    Algo_SHA256='SHA256',
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 4: project

| project  
    MD5,
    Algo_MD5,
    SHA1,
    Algo_SHA1,
    SHA256,
    Algo_SHA256,
    ThreatActors,
    Sources,
    RecommendedActions,
    Roles,
    Country,
    name,
    Description,
    ConfidenceScore,
    SecurityVendors,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Algo_MD5project
Algo_SHA1project
Algo_SHA256project
ConfidenceScoreproject
Countryproject
Descriptionproject
IndicatorIDproject
MD5project
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SHA1project
SHA256project
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
createdproject
modifiedproject
nameproject
valid_fromproject

CYFIRMA - High severity Trojan File Hash Indicators with Monitor Action 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.

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

"This KQL query extracts file hash indicators associated with Trojan activity from the CyfirmaIndicators_CL table. It specifically targets indicators containing file hashes linked to Trojan behavior and retrieves MD5, SHA1, and SHA256 values. The query also includes contextual threat intelligence such as threat actors, tags, sources, and geolocation information."

MITRE ATT&CK Coverage

Rule Body

id: 4afd8960-8bee-4cac-bb5e-a4f200b1f9f3
name: CYFIRMA - High severity Trojan File Hash Indicators with Monitor Action Rule
description: |
  "This KQL query extracts file hash indicators associated with Trojan activity from the CyfirmaIndicators_CL table. 
  It specifically targets indicators containing file hashes linked to Trojan behavior and retrieves MD5, SHA1, and SHA256 values. 
  The query also includes contextual threat intelligence such as threat actors, tags, sources, and geolocation information."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  //Trojan File Hash Indicators with Monitor Action
  let timeFrame = 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'Trojan')
  | extend MD5 = extract(@"file:hashes\.md5\s*=\s*'([a-fA-F0-9]{32})'", 1, pattern)
  | extend SHA1 = extract(@"file:hashes\.'SHA-1'\s*=\s*'([a-fA-F0-9]{40})'", 1, pattern)
  | extend SHA256 = extract(@"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'", 1, pattern)
  | extend
      Algo_MD5='md5',
      Algo_SHA1= 'SHA1',
      Algo_SHA256='SHA256',
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project  
      MD5,
      Algo_MD5,
      SHA1,
      Algo_SHA1,
      SHA256,
      Algo_SHA256,
      ThreatActors,
      Sources,
      RecommendedActions,
      Roles,
      Country,
      name,
      Description,
      ConfidenceScore,
      SecurityVendors,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - InitialAccess
  - Execution
  - Persistence
  - DefenseEvasion
  - CommandAndControl
  - CredentialAccess
relevantTechniques:
  - T1566
  - T1204
  - T1547
  - T1027
  - T1071
  - T1003
  - T1566.001
  - T1547.001
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Trojan File Hash Indicators with Monitor Action - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  created: created
  modified: modified
  ValidFrom: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
entityMappings:
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_MD5
      - identifier: Value
        columnName: MD5
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA1
      - identifier: Value
        columnName: SHA1
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA256
      - identifier: Value
        columnName: SHA256
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

CyfirmaIndicators_CL

Stage 2: where

| where ConfidenceScore >= 80
    and TimeGenerated between (ago(timeFrame) .. now())
    and pattern contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'Trojan')

Stage 3: extend (4 consecutive steps)

| extend MD5 = extract(@"file:hashes\.md5\s*=\s*'([a-fA-F0-9]{32})'", 1, pattern)
| extend SHA1 = extract(@"file:hashes\.'SHA-1'\s*=\s*'([a-fA-F0-9]{40})'", 1, pattern)
| extend SHA256 = extract(@"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'", 1, pattern)
| extend
    Algo_MD5='md5',
    Algo_SHA1= 'SHA1',
    Algo_SHA256='SHA256',
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 4: project

| project  
    MD5,
    Algo_MD5,
    SHA1,
    Algo_SHA1,
    SHA256,
    Algo_SHA256,
    ThreatActors,
    Sources,
    RecommendedActions,
    Roles,
    Country,
    name,
    Description,
    ConfidenceScore,
    SecurityVendors,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Algo_MD5project
Algo_SHA1project
Algo_SHA256project
ConfidenceScoreproject
Countryproject
Descriptionproject
IndicatorIDproject
MD5project
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SHA1project
SHA256project
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
createdproject
modifiedproject
nameproject
valid_fromproject

CYFIRMA - Medium Severity Attack Surface based Vulnerabilities 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.

Severity
medium
Time window
5m
Group by
name
Source
github.com/Azure/Azure-Sentinel

"This rule detects medium severity attack surface-based vulnerabilities from CYFIRMA's vulnerability intelligence data. It identifies vulnerabilities with a confidence score of 50 or higher, excluding those categorized as 'ASSET_VULNERABILITY', and generates alerts for assets that may be at risk."

MITRE ATT&CK Coverage

Rule Body

id: 4c1b282b-62f1-4783-bf40-94c44f0ae630
name: CYFIRMA - Medium Severity Attack Surface based Vulnerabilities Rule
description: |
  "This rule detects medium severity attack surface-based vulnerabilities from CYFIRMA's vulnerability intelligence data. It identifies vulnerabilities with a confidence score of 50 or higher, excluding those categorized as 'ASSET_VULNERABILITY', and generates alerts for assets that may be at risk."
version: 1.0.1
kind: Scheduled
severity: Medium
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaVulnerabilitiesIntelDC
    dataTypes:
      - CyfirmaVulnerabilities_CL
query: |
  // Medium severity - Attack Surface based Vulnerabilities
  let timeFrame= 5m;
  CyfirmaVulnerabilities_CL
  | extend parsed = parse_json(extensions)
  | extend extensionKeys = bag_keys(parsed)
  | mv-expand extensionKeys
  | extend extensionKeyStr = tostring(extensionKeys)
  | extend ext = parsed[extensionKeyStr]
  | extend props = ext.properties
  | extend 
      attack_complexity         = tostring(props.attack_complexity),
      cvss_score                = toreal(props.cvss_score),
      integrity_impact          = tostring(props.integrity_impact),
      impact_score              = tostring(props.impact_score),
      attack_vector             = tostring(props.attack_vector),
      privileges_required       = tostring(props.privileges_required),
      cvss_version              = tostring(props.cvss_version),
      user_interaction          = tostring(props.user_interaction),
      cvss_vector               = tostring(props.cvss_vector),
      scope                     = tostring(props.scope),
      confidentiality_impact    = tostring(props.confidentiality_impact),
      exploitability_score      = toreal(props.exploitability_score),
      products                  = tostring(props.products),
      technologies              = tostring(props.technologies),
      vendors                   = tostring(props.vendors),
      confidence_score          = toint(confidence),
      servers                   = tostring(props.servers),
      vulnerability_type        = tostring(props.vulnerability_type),
      vulnerability_category        = tostring(props.vulnerability_category),
      NetworkIPs                = tostring(props.ips),
      ProviderName              ='CYFIRMA',
      ProductName               ='DeCYFIR/DeTCT'
  | summarize arg_max(
                  integrity_impact,
                  TimeGenerated, 
                  id,
                  description,
                  confidence_score,
                  created,
                  modified,
                  attack_complexity,
                  cvss_score,
                  impact_score,
                  attack_vector,
                  privileges_required,
                  cvss_version,
                  user_interaction,
                  cvss_vector,
                  scope,
                  confidentiality_impact,
                  exploitability_score,
                  products,
                  technologies,
                  vendors,
                  ProviderName,
                  ProductName,
                  servers,
                  NetworkIPs,
                  vulnerability_type,
                  vulnerability_category
              )
      by name
  | where confidence_score >= 60 and vulnerability_category == 'ATTACK_SURFACE_VULNERABILITY' and TimeGenerated between (ago(timeFrame) .. now())
  | project 
      TimeGenerated,
      name,
      confidence_score,
      integrity_impact,
      attack_complexity,
      cvss_score,
      impact_score,
      attack_vector,
      UID = id,
      description,
      created,
      modified,
      privileges_required,
      cvss_version,
      user_interaction,
      cvss_vector,
      scope,
      confidentiality_impact,
      exploitability_score,
      products,
      technologies,
      vendors,
      ProviderName,
      ProductName,
      servers,
      NetworkIPs,
      vulnerability_type,
      vulnerability_category
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: false
tactics:
  - Execution
  - LateralMovement
  - PrivilegeEscalation
  - InitialAccess
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1059
  - T1203
  - T1210
  - T1068
  - T1190
  - T1133
  - T1003
  - T1553
  - T1548.002
  - T1021.002
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Medium Severity Attack Surface based Vulnerability Identified - {{name}} "
  alertDescriptionFormat: "{{description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
customDetails:
  TimeGenerated: TimeGenerated
  CVE: name
  ConfidenceScore: confidence_score
  IntegrityImpact: integrity_impact
  AttackComplexity: attack_complexity
  CVSSScore: cvss_score
  ImpactScore: impact_score
  AttackVector: attack_vector
  ConfidentialImpact: confidentiality_impact
  PrivilegesRequired: privileges_required
  CVSSVersion: cvss_version
  UserInteraction: user_interaction
  CVSSVector: scope
  scope: exploitability_score
  ExploitabilityScore: modified
  Modified: products
  Products: technologies
  Vendors: vendors
  Technologies: technologies
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

CyfirmaVulnerabilities_CL

Stage 2: extend

| extend parsed = parse_json(extensions)

Stage 3: extend

| extend extensionKeys = bag_keys(parsed)

Stage 4: mv-expand

| mv-expand extensionKeys

Stage 5: extend (4 consecutive steps)

| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend 
    attack_complexity         = tostring(props.attack_complexity),
    cvss_score                = toreal(props.cvss_score),
    integrity_impact          = tostring(props.integrity_impact),
    impact_score              = tostring(props.impact_score),
    attack_vector             = tostring(props.attack_vector),
    privileges_required       = tostring(props.privileges_required),
    cvss_version              = tostring(props.cvss_version),
    user_interaction          = tostring(props.user_interaction),
    cvss_vector               = tostring(props.cvss_vector),
    scope                     = tostring(props.scope),
    confidentiality_impact    = tostring(props.confidentiality_impact),
    exploitability_score      = toreal(props.exploitability_score),
    products                  = tostring(props.products),
    technologies              = tostring(props.technologies),
    vendors                   = tostring(props.vendors),
    confidence_score          = toint(confidence),
    servers                   = tostring(props.servers),
    vulnerability_type        = tostring(props.vulnerability_type),
    vulnerability_category        = tostring(props.vulnerability_category),
    NetworkIPs                = tostring(props.ips),
    ProviderName              ='CYFIRMA',
    ProductName               ='DeCYFIR/DeTCT'

Stage 6: summarize

| summarize arg_max(
                integrity_impact,
                TimeGenerated, 
                id,
                description,
                confidence_score,
                created,
                modified,
                attack_complexity,
                cvss_score,
                impact_score,
                attack_vector,
                privileges_required,
                cvss_version,
                user_interaction,
                cvss_vector,
                scope,
                confidentiality_impact,
                exploitability_score,
                products,
                technologies,
                vendors,
                ProviderName,
                ProductName,
                servers,
                NetworkIPs,
                vulnerability_type,
                vulnerability_category
            )
    by name

Stage 7: where

| where confidence_score >= 60 and vulnerability_category == 'ATTACK_SURFACE_VULNERABILITY' and TimeGenerated between (ago(timeFrame) .. now())

Stage 8: project

| project 
    TimeGenerated,
    name,
    confidence_score,
    integrity_impact,
    attack_complexity,
    cvss_score,
    impact_score,
    attack_vector,
    UID = id,
    description,
    created,
    modified,
    privileges_required,
    cvss_version,
    user_interaction,
    cvss_vector,
    scope,
    confidentiality_impact,
    exploitability_score,
    products,
    technologies,
    vendors,
    ProviderName,
    ProductName,
    servers,
    NetworkIPs,
    vulnerability_type,
    vulnerability_category

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
NetworkIPsproject
ProductNameproject
ProviderNameproject
TimeGeneratedproject
UIDproject
attack_complexityproject
attack_vectorproject
confidence_scoreproject
confidentiality_impactproject
createdproject
cvss_scoreproject
cvss_vectorproject
cvss_versionproject
descriptionproject
exploitability_scoreproject
impact_scoreproject
integrity_impactproject
modifiedproject
nameproject
privileges_requiredproject
productsproject
scopeproject
serversproject
technologiesproject
user_interactionproject
vendorsproject
vulnerability_categoryproject
vulnerability_typeproject

Detect Potential Kerberoast Activities

#
Status
available
Severity
medium
Time window
2m
Group by
AccountSid, AccountUpn, DeviceName
Source
github.com/Azure/Azure-Sentinel

This query aim to detect if someone requests service tickets (where count => maxcount) The query requires trimming to set a baseline level for MaxCount
Mitre Technique: Kerberoasting (T1558.003) @MattiasBorg82

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 12134de5-361b-427c-a1a0-d43f40a593c4
name: Detect Potential Kerberoast Activities
description: |
  This query aim to detect if someone requests service tickets (where count => maxcount)
  The query requires trimming to set a baseline level for MaxCount  
  Mitre Technique: Kerberoasting (T1558.003)
  @MattiasBorg82
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - IdentityLogonEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1558.003
tag:
  - Kerberoast
query: |
  let MaxCount = 70; //Number of requests per 2 minute timeframe, depending on org size.
  IdentityLogonEvents
  | where ActionType == "LogonSuccess"
  | where Protocol == "Kerberos"
  | extend json = todynamic(parse_json(tostring(AdditionalFields)))
  | extend SPN = json.Spns,
         AttackTechniques = json.AttackTechniques
        | project-away json
  | where isnotempty(SPN)
  | where AttackTechniques has "T1558.003"
  | mv-expand SPN
          | extend SPNType = tostring(extract(@"^\w+",0,tostring(SPN)))
  | distinct tostring(SPN), DeviceName, AccountUpn, AccountSid, bin(TimeGenerated, 2m), ReportId, tostring(AttackTechniques)
  | summarize count(), SPNS=(make_list(SPN, 100000)),ReportId=tostring((make_list(ReportId, 100000))[0]) by AccountUpn,AccountSid,DeviceName, bin(TimeGenerated, 2m), tostring(AttackTechniques)
  | extend SPNS = (replace_regex(tostring(SPNS), @'[^\w+-\/]+', '')) 
  | where count_ >= MaxCount
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
  | extend AccountDomain = tostring(split(AccountUpn, '@')[1]), AccountName = tostring(split(AccountUpn, '@')[0])
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUpn
      - identifier: DnsDomain
        columnName: AccountDomain
      - identifier: Name
        columnName: AccountName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let MaxCount = 70;

Stage 1: source

IdentityLogonEvents

Stage 2: where

| where ActionType == "LogonSuccess"

Stage 3: where

| where Protocol == "Kerberos"

Stage 4: extend

| extend json = todynamic(parse_json(tostring(AdditionalFields)))

Stage 5: extend

| extend SPN = json.Spns,
       AttackTechniques = json.AttackTechniques

Stage 6: project-away

| project-away json

Stage 7: where

| where isnotempty(SPN)

Stage 8: where

| where AttackTechniques has "T1558.003"

Stage 9: mv-expand

| mv-expand SPN

Stage 10: extend

| extend SPNType = tostring(extract(@"^\w+",0,tostring(SPN)))

Stage 11: distinct

| distinct tostring(SPN), DeviceName, AccountUpn, AccountSid, bin(TimeGenerated, 2m), ReportId, tostring(AttackTechniques)

Stage 12: summarize

| summarize count(), SPNS=(make_list(SPN, 100000)),ReportId=tostring((make_list(ReportId, 100000))[0]) by AccountUpn,AccountSid,DeviceName, bin(TimeGenerated, 2m), tostring(AttackTechniques)

Stage 13: extend

| extend SPNS = (replace_regex(tostring(SPNS), @'[^\w+-\/]+', ''))

Stage 14: where

| where count_ >= MaxCount

Stage 15: extend (3 consecutive steps)

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
| extend AccountDomain = tostring(split(AccountUpn, '@')[1]), AccountName = tostring(split(AccountUpn, '@')[0])
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • LogonSuccess corpus 2 (kusto 2)
field:"ActionType" kind:eq value:"LogonSuccess"
AttackTechniquesmatch
  • T1558.003 transforms: term
field:"AttackTechniques" kind:match value:"T1558.003"
Protocoleq
  • Kerberos corpus 2 (kusto 2)
field:"Protocol" kind:eq value:"Kerberos"
SPNis_not_null
  • (no value, null check)
field:"SPN" kind:is_not_null
count_ge
  • 70 transforms: number
field:"count_" kind:ge value:"70"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountSidsummarize
AccountUpnsummarize
DeviceNamesummarize
ReportIdsummarize
SPNSextend
HostNameextend
DnsDomainextend
AccountDomainextend
AccountNameextend

High count of failed logons by a user

#
Severity
medium
Time window
10m
Group by
Computer, csUserName, sIP
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies when 100 or more failed attempts by a given user in 10 minutes occur on the IIS Server. This could be indicative of attempted brute force based on known account information. This could also simply indicate a misconfigured service or device. References: IIS status code mapping - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0 Win32 Status code mapping - https://msdn.microsoft.com/library/cc231199.aspx'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 884c4957-70ea-4f57-80b9-1bca3890315b
name: High count of failed logons by a user
description: |
  'Identifies when 100 or more failed attempts by a given user in 10 minutes occur on the IIS Server.
  This could be indicative of attempted brute force based on known account information.
  This could also simply indicate a misconfigured service or device.
  References:
  IIS status code mapping - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0
  Win32 Status code mapping - https://msdn.microsoft.com/library/cc231199.aspx'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureMonitor(IIS)
    dataTypes:
      - W3CIISLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let timeBin = 10m;
  let failedThreshold = 100;
  W3CIISLog
  | where scStatus in ("401","403")
  | where csUserName != "-"
  // Handling Exchange specific items in IIS logs to remove the unique log identifier in the URI
  | extend csUriQuery = iff(csUriQuery startswith "MailboxId=", tostring(split(csUriQuery, "&")[0]) , csUriQuery )
  | extend csUriQuery = iff(csUriQuery startswith "X-ARR-CACHE-HIT=", strcat(tostring(split(csUriQuery, "&")[0]),tostring(split(csUriQuery, "&")[1])) , csUriQuery )
  | extend scStatusFull = strcat(scStatus, ".",scSubStatus)
  // Map common IIS codes
  | extend scStatusFull_Friendly = case(
  scStatusFull == "401.0", "Access denied.",
  scStatusFull == "401.1", "Logon failed.",
  scStatusFull == "401.2", "Logon failed due to server configuration.",
  scStatusFull == "401.3", "Unauthorized due to ACL on resource.",
  scStatusFull == "401.4", "Authorization failed by filter.",
  scStatusFull == "401.5", "Authorization failed by ISAPI/CGI application.",
  scStatusFull == "403.0", "Forbidden.",
  scStatusFull == "403.4", "SSL required.",
  "See - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0")
  // Mapping to Hex so can be mapped using website in comments above
  | extend scWin32Status_Hex = tohex(tolong(scWin32Status))
  // Map common win32 codes
  | extend scWin32Status_Friendly = case(
  scWin32Status_Hex =~ "775", "The referenced account is currently locked out and cannot be logged on to.",
  scWin32Status_Hex =~ "52e", "Logon failure: Unknown user name or bad password.",
  scWin32Status_Hex =~ "532", "Logon failure: The specified account password has expired.",
  scWin32Status_Hex =~ "533", "Logon failure: Account currently disabled.",
  scWin32Status_Hex =~ "2ee2", "The request has timed out.",
  scWin32Status_Hex =~ "0", "The operation completed successfully.",
  scWin32Status_Hex =~ "1", "Incorrect function.",
  scWin32Status_Hex =~ "2", "The system cannot find the file specified.",
  scWin32Status_Hex =~ "3", "The system cannot find the path specified.",
  scWin32Status_Hex =~ "4", "The system cannot open the file.",
  scWin32Status_Hex =~ "5", "Access is denied.",
  scWin32Status_Hex =~ "8009030e", "SEC_E_NO_CREDENTIALS",
  scWin32Status_Hex =~ "8009030C", "SEC_E_LOGON_DENIED",
  "See - https://msdn.microsoft.com/library/cc231199.aspx")
  // decode URI when available
  | extend decodedUriQuery = url_decode(csUriQuery)
  // Count of failed logons by a user
  | summarize makeset(decodedUriQuery), makeset(cIP), makeset(sSiteName), makeset(sPort), makeset(csUserAgent), makeset(csMethod), makeset(csUriQuery), makeset(scStatusFull), makeset(scStatusFull_Friendly), makeset(scWin32Status_Hex), makeset(scWin32Status_Friendly), FailedConnectionsCount = count() by bin(TimeGenerated, timeBin), csUserName, Computer, sIP
  | where FailedConnectionsCount >= failedThreshold
  | project TimeGenerated, csUserName, set_decodedUriQuery, Computer, set_sSiteName, sIP, set_cIP, set_sPort, set_csUserAgent, set_csMethod, set_scStatusFull, set_scStatusFull_Friendly, set_scWin32Status_Hex, set_scWin32Status_Friendly, FailedConnectionsCount
  | order by FailedConnectionsCount
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: csUserName
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain  
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let timeBin = 10m;
let failedThreshold = 100;

Stage 1: source

W3CIISLog

Stage 2: where

| where scStatus in ("401","403")

Stage 3: where

| where csUserName != "-"

Stage 4: extend (7 consecutive steps)

| extend csUriQuery = iff(csUriQuery startswith "MailboxId=", tostring(split(csUriQuery, "&")[0]) , csUriQuery )
| extend csUriQuery = iff(csUriQuery startswith "X-ARR-CACHE-HIT=", strcat(tostring(split(csUriQuery, "&")[0]),tostring(split(csUriQuery, "&")[1])) , csUriQuery )
| extend scStatusFull = strcat(scStatus, ".",scSubStatus)
| extend scStatusFull_Friendly = case(
scStatusFull == "401.0", "Access denied.",
scStatusFull == "401.1", "Logon failed.",
scStatusFull == "401.2", "Logon failed due to server configuration.",
scStatusFull == "401.3", "Unauthorized due to ACL on resource.",
scStatusFull == "401.4", "Authorization failed by filter.",
scStatusFull == "401.5", "Authorization failed by ISAPI/CGI application.",
scStatusFull == "403.0", "Forbidden.",
scStatusFull == "403.4", "SSL required.",
"See - https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0")
| extend scWin32Status_Hex = tohex(tolong(scWin32Status))
| extend scWin32Status_Friendly = case(
scWin32Status_Hex =~ "775", "The referenced account is currently locked out and cannot be logged on to.",
scWin32Status_Hex =~ "52e", "Logon failure: Unknown user name or bad password.",
scWin32Status_Hex =~ "532", "Logon failure: The specified account password has expired.",
scWin32Status_Hex =~ "533", "Logon failure: Account currently disabled.",
scWin32Status_Hex =~ "2ee2", "The request has timed out.",
scWin32Status_Hex =~ "0", "The operation completed successfully.",
scWin32Status_Hex =~ "1", "Incorrect function.",
scWin32Status_Hex =~ "2", "The system cannot find the file specified.",
scWin32Status_Hex =~ "3", "The system cannot find the path specified.",
scWin32Status_Hex =~ "4", "The system cannot open the file.",
scWin32Status_Hex =~ "5", "Access is denied.",
scWin32Status_Hex =~ "8009030e", "SEC_E_NO_CREDENTIALS",
scWin32Status_Hex =~ "8009030C", "SEC_E_LOGON_DENIED",
"See - https://msdn.microsoft.com/library/cc231199.aspx")
| extend decodedUriQuery = url_decode(csUriQuery)
csUriQuery =
ifcsUriQuery startswith "MailboxId="tostring(split(csUriQuery, "&")[0])
elsecsUriQuery

Stage 5: summarize

| summarize makeset(decodedUriQuery), makeset(cIP), makeset(sSiteName), makeset(sPort), makeset(csUserAgent), makeset(csMethod), makeset(csUriQuery), makeset(scStatusFull), makeset(scStatusFull_Friendly), makeset(scWin32Status_Hex), makeset(scWin32Status_Friendly), FailedConnectionsCount = count() by bin(TimeGenerated, timeBin), csUserName, Computer, sIP

Stage 6: where

| where FailedConnectionsCount >= failedThreshold

Stage 7: project

| project TimeGenerated, csUserName, set_decodedUriQuery, Computer, set_sSiteName, sIP, set_cIP, set_sPort, set_csUserAgent, set_csMethod, set_scStatusFull, set_scStatusFull_Friendly, set_scWin32Status_Hex, set_scWin32Status_Friendly, FailedConnectionsCount

Stage 8: sort

| order by FailedConnectionsCount

Stage 9: extend (3 consecutive steps)

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(csUserName, "@")[0]), AccountUPNSuffix = tostring(split(csUserName, "@")[1])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
FailedConnectionsCountproject
TimeGeneratedproject
csUserNameproject
sIPproject
set_cIPproject
set_csMethodproject
set_csUserAgentproject
set_decodedUriQueryproject
set_sPortproject
set_sSiteNameproject
set_scStatusFullproject
set_scStatusFull_Friendlyproject
set_scWin32Status_Friendlyproject
set_scWin32Status_Hexproject
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNameextend
AccountUPNSuffixextend

Keeper Security - User MFA Changed

#
Status
available
Severity
informational
Source
github.com/Azure/Azure-Sentinel

'Creates an informational incident based on Keeper Security User MFA Changed data in Microsoft Sentinel'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 75ffc8a4-86db-4f48-8506-cb4c049be484
name: Keeper Security - User MFA Changed
description: |
  'Creates an informational incident based on Keeper Security User MFA Changed data in Microsoft Sentinel'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: KeeperSecurityPush2
    dataTypes: 
      - KeeperSecurityEventNewLogs_CL
suppressionDuration: PT5H
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
tactics:
- Persistence
relevantTechniques:
- T1556
query: |
  KeeperSecurityEventNewLogs_CL
  | where AuditEvent in (```set_two_factor_off```, ```set_two_factor_on```)
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
alertDetailsOverride:
  alertDisplayNameFormat: "{{AuditEvent}} on {{RemoteAddress}}"
  alertDescriptionFormat: "{{AuditEvent}} has been captured in the Keeper Security Event Logs"
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: Username
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: RemoteAddress
version: 1.0.3
kind: NRT

Stages and Predicates

Stage 1: source

KeeperSecurityEventNewLogs_CL

Stage 2: where

| where AuditEvent in (```set_two_factor_off```, ```set_two_factor_on```)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AuditEventin
  • set_two_factor_off
  • set_two_factor_on
field:"AuditEvent" kind:in

Malware Detection Settings Updated

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

Detects when malware detection settings are updated.

Rule Body

id: effd8410-3119-41c8-a228-9c0c8ce10d67
name: Malware Detection Settings Updated
description: Detects when malware detection settings are updated.
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 == 42290\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 == 42290

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

Multiple admin membership removals from newly created admin.

#
Status
available
Severity
medium
Time window
7d
Group by
$left.Target, $right.Initiator, Initiator, OperationName, Result, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access. Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: cda5928c-2c1e-4575-9dfa-07568bc27a4f
name: Multiple admin membership removals from newly created admin.
description: |
  'This query detects when newly created Global admin removes multiple existing global admins which can be an attempt by adversaries to lock down organization and retain sole access. 
   Investigate reasoning and intention of multiple membership removal by new Global admins and take necessary actions accordingly.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Impact
relevantTechniques:
  - T1531
tags:
  - DEV-0537
query: |
  let lookback = 7d; 
  let timeframe = 1h; 
  let GlobalAdminsRemoved = AuditLogs 
  | where TimeGenerated > ago(timeframe) 
  | where Category =~ "RoleManagement" 
  | where AADOperationType in ("Unassign", "RemoveEligibleRole") 
  | where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role") 
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend Target = tostring(TargetResource.userPrincipalName),
                 props = TargetResource.modifiedProperties
    )
  | mv-apply Property = props on 
        (
            where Property.displayName =~ "Role.DisplayName"
            | extend RoleName = trim('"',tostring(Property.oldValue))
        )
  | where RoleName =~ "Global Administrator" // Add other Privileged role if applicable
  | 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 Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName) 
  | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"  // Filtering PIM events  
  | summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target,100) by OperationName, RoleName, Initiator, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, Result; 
  let GlobalAdminsAdded = AuditLogs 
  | where TimeGenerated > ago(lookback) 
  | where Category =~ "RoleManagement" 
  | where AADOperationType in ("Assign", "AssignEligibleRole") 
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result == "success" 
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend Target = tostring(TargetResource.userPrincipalName),
                 props = TargetResource.modifiedProperties
    )
  | mv-apply Property = props on 
        (
            where Property.displayName =~ "Role.DisplayName"
            | extend RoleName = trim('"',tostring(Property.newValue))
        )
  | where RoleName =~ "Global Administrator" // Add other Privileged role if applicable
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, tostring(InitiatedBy.user.userPrincipalName)) 
  | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"  // Filtering PIM events 
  | summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result;
  GlobalAdminsAdded 
  | join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator 
  | where AddedGlobalAdminTime < RemovedGlobalAdminTime 
  | extend NoofAdminsRemoved = array_length(TargetAdmins) 
  | where NoofAdminsRemoved > 1
  | project AddedGlobalAdminTime, Initiator, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, Target, RemovedGlobalAdminTime, TargetAdmins, NoofAdminsRemoved
  | extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0])
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - 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.3
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 7d;
let timeframe = 1h;

let GlobalAdminsAdded is inlined into the numbered stages below.

Let Binding: GlobalAdminsRemoved used in Stage 13

let GlobalAdminsRemoved = AuditLogs 
| where TimeGenerated > ago(timeframe) 
| where Category =~ "RoleManagement" 
| where AADOperationType in ("Unassign", "RemoveEligibleRole") 
| where ActivityDisplayName has_any ("Remove member from role", "Remove eligible member from role") 
| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend Target = tostring(TargetResource.userPrincipalName),
               props = TargetResource.modifiedProperties
  )
| mv-apply Property = props on 
      (
          where Property.displayName =~ "Role.DisplayName"
          | extend RoleName = trim('"',tostring(Property.oldValue))
      )
| where RoleName =~ "Global Administrator"
| 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 Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName) 
| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
| summarize RemovedGlobalAdminTime = max(TimeGenerated), TargetAdmins = make_set(Target,100) by OperationName, RoleName, Initiator, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, Result;

Stages 1 to 12 define let GlobalAdminsAdded as the rule's main pipeline source. Stages 13 to 19 use it.

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(lookback)

Stage 3: where

| where Category =~ "RoleManagement"

Stage 4: where

| where AADOperationType in ("Assign", "AssignEligibleRole")

Stage 5: where

| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role") and Result == "success"

Stage 6: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend Target = tostring(TargetResource.userPrincipalName),
               props = TargetResource.modifiedProperties
  )

Stage 7: kusto:mv-apply

| mv-apply Property = props on 
      (
          where Property.displayName =~ "Role.DisplayName"
          | extend RoleName = trim('"',tostring(Property.newValue))
      )

Stage 8: where

| where RoleName =~ "Global Administrator"

Stage 9: extend

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)

Stage 10: extend

| extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, tostring(InitiatedBy.user.userPrincipalName))

Stage 11: where

| where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"

Stage 12: summarize

| summarize AddedGlobalAdminTime = max(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result

Stage 13: join

GlobalAdminsAdded
| join kind= inner GlobalAdminsRemoved on $left.Target == $right.Initiator

Stage 14: where

where AddedGlobalAdminTime < RemovedGlobalAdminTime

Stage 15: extend

extend NoofAdminsRemoved

Stage 16: where

where NoofAdminsRemoved > 1

Stage 17: project

project AddedGlobalAdminTime, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, NoofAdminsRemoved, RemovedGlobalAdminTime, Target, TargetAdmins

Stage 18: extend

extend TargetName, TargetUPNSuffix

Stage 19: extend

extend InitiatedByName, InitiatedByUPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypein
  • Assign corpus 6 (kusto 6)
  • AssignEligibleRole corpus 5 (kusto 5)
  • RemoveEligibleRole
  • Unassign
field:"AADOperationType" kind:in
ActivityDisplayNamematch
  • Add eligible member to role transforms: term corpus 9 (kusto 9)
  • Add member to role transforms: term corpus 9 (kusto 9)
  • Remove eligible member from role transforms: term
  • Remove member from role transforms: term
field:"azure_ad::activity_display_name" kind:match
Categoryeq
  • RoleManagement corpus 17 (kusto 16, sigma 1)
field:"Category" kind:eq value:"RoleManagement"
Initiatorne
  • MS-PIM
  • MS-PIM-Fairfax
field:"Initiator" kind:ne
NoofAdminsRemovedgt
  • 1 transforms: number
field:"NoofAdminsRemoved" kind:gt value:"1"
Property.displayNameeq
  • Role.DisplayName transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"Role.DisplayName"
Resulteq
  • success corpus 4 (kusto 4)
field:"Result" kind:eq value:"success"
RoleNameeq
  • Global Administrator
field:"RoleName" kind:eq value:"Global Administrator"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AddedGlobalAdminTimeproject
InitiatingAadUserIdproject
InitiatingAppNameproject
InitiatingAppServicePrincipalIdproject
InitiatingIpAddressproject
InitiatingUserPrincipalNameproject
Initiatorproject
NoofAdminsRemovedproject
RemovedGlobalAdminTimeproject
Targetproject
TargetAdminsproject
TargetNameextend
TargetUPNSuffixextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

NetworkSecurityGroups Alert From Prancer

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

'High severity network security groups alerts found by Prancer.'

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Rule Body

id: a8babf91-b844-477c-8abf-d31e3df74933
name: NetworkSecurityGroups Alert From Prancer
description: |
  'High severity network security groups alerts found by Prancer.'
severity: High
requiredDataConnectors:
- connectorId: PrancerLogData
  dataTypes:
    - prancer_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - Reconnaissance
relevantTechniques:
  - T1595
query: |
  union prancer_CL
  | where deviceProduct_s == 'azure'
  | where parse_json(data_data_snapshots_s)[0].type == 'Microsoft.Network/networkSecurityGroups' //and parse_json(data_data_snapshots_s)[0].region != ''
  | where data_data_severity_s == 'High' and data_data_result_s == 'failed'
  | extend snapshot = parse_json(data_data_snapshots_s)
  | mv-expand snapshot 
  | extend
      id = tostring(snapshot.id),
      structure = tostring(snapshot.structure),
      reference = tostring(snapshot.reference),
      source = tostring(snapshot.source),
      collection = tostring(snapshot.collection),
      type = tostring(snapshot.type),
      region = tostring(snapshot.region),
      resourceTypes = tostring(snapshot.resourceTypes),
      path = tostring(snapshot.path)


customDetails:
entityMappings:
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: path
alertDetailsOverride:
  alertDisplayNameFormat: "{{data_data_message_s}}"  
  alertDescriptionFormat: "{{data_data_description_s}}"
  alertSeverityColumnName: "{{data_data_severity_s}}"
  alertDynamicProperties:
    - alertProperty: RemediationSteps
      value: data_data_remediation_description_s
version: 1.0.2
kind: Scheduled


Stages and Predicates

Stage 1: union

union of 1 branches

Stage 2: source

prancer_CL

Stage 3: where

where deviceProduct_s == "azure"

Stage 4: where

where parse_json(data_data_snapshots_s)[0].type == "Microsoft.Network/networkSecurityGroups"

Stage 5: where

where data_data_result_s == "failed" and data_data_severity_s == "High"

Stage 6: extend

extend snapshot

Stage 7: mv-expand

mv-expand snapshot

Stage 8: extend

extend collection, id, path, reference, region, resourceTypes, source, structure, type

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
data_data_result_seq
  • failed
field:"data_data_result_s" kind:eq value:"failed"
data_data_severity_seq
  • High
field:"data_data_severity_s" kind:eq value:"High"
data_data_snapshots_s[0].typeeq
  • Microsoft.Network/networkSecurityGroups transforms: kusto_accessor_expr:{"expression":"parse_json(data_data_snapshots_s)[0].type","field":"data_data_snapshots_s[0].type"}, kusto_dynamic_path
field:"data_data_snapshots_s[0].type" kind:eq value:"Microsoft.Network/networkSecurityGroups"
deviceProduct_seq
  • azure
field:"deviceProduct_s" kind:eq value:"azure"

Output Fields

These fields are emitted when the rule matches.

FieldSource
snapshotextend
collectionextend
idextend
pathextend
referenceextend
regionextend
resourceTypesextend
sourceextend
structureextend
typeextend

New EXE deployed via Default Domain or Default Domain Controller Policies

#
Status
available
Severity
high
Time window
14d
Group by
CommandLine, Computer, NewProcessName, Process
Source
github.com/Azure/Azure-Sentinel

This detection highlights executables deployed to hosts via either the Default Domain or Default Domain Controller Policies. These policies apply to all hosts or Domain Controllers and best practice is that these policies should not be used for deployment of files. A threat actor may use these policies to deploy files or scripts to all hosts in a domain.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 05b4bccd-dd12-423d-8de4-5a6fb526bb4f
name: New EXE deployed via Default Domain or Default Domain Controller Policies
description: |
  'This detection highlights executables deployed to hosts via either the Default Domain or Default Domain Controller Policies. These policies apply to all hosts or Domain Controllers and best practice is that these policies should not be used for deployment of files.
  A threat actor may use these policies to deploy files or scripts to all hosts in a domain.'
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
  - LateralMovement
relevantTechniques:
  - T1072
  - T1570
query: |
  let known_processes = (
    SecurityEvent
    // If adjusting Query Period or Frequency update these
    | where TimeGenerated between(ago(14d)..ago(1d))
    | where EventID == 4688
    | where NewProcessName has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
    | summarize by Process);
    SecurityEvent
    // If adjusting Query Period or Frequency update these
    | where TimeGenerated > ago(1d)
    | where EventID == 4688
    | where NewProcessName has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
    | where Process !in (known_processes)
    // This will likely apply to multiple hosts so summarize these data
    | summarize FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Process, NewProcessName, CommandLine, Computer
    | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
    | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: known_processes used in Stage 5

let known_processes = (
  SecurityEvent
  | where TimeGenerated between(ago(14d)..ago(1d))
  | where EventID == 4688
  | where NewProcessName has_any ("Policies\\{6AC1786C-016F-11D2-945F-00C04fB984F9}", "Policies\\{31B2F340-016D-11D2-945F-00C04FB984F9}")
  | summarize by Process);

Stage 1: source

SecurityEvent

Stage 2: where

where TimeGenerated > ago(86400s)

Stage 3: where

where EventID == 4688

Stage 4: where

where (NewProcessName has @"Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9}" or NewProcessName has @"Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}")

Stage 5: where

where not (Process == "known_processes")

Stage 6: summarize

summarize FirstSeen, LastSeen by Process, NewProcessName, CommandLine, Computer

Stage 7: extend

extend DomainIndex, HostName

Stage 8: extend

extend HostNameDomain
HostNameDomain =
ifDomainIndex != -1substring(Computer, (DomainIndex + 1))
elseComputer

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
NewProcessNamematch
  • Policies\{31B2F340-016D-11D2-945F-00C04FB984F9} transforms: term
  • Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9} transforms: term
field:"Image" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Processeqknown_processesexcludes:Process field:"Process" value:"known_processes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
Computersummarize
FirstSeensummarize
LastSeensummarize
NewProcessNamesummarize
Processsummarize
DomainIndexextend
HostNameextend
HostNameDomainextend

New user created and added to the built-in administrators group

#
Severity
low
Time window
1d
Group by
CreatedUserSid
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies when a user account was created and then added to the builtin Administrators group in the same day. This should be monitored closely and all additions reviewed.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: aa1eff90-29d4-49dc-a3ea-b65199f516db
name: New user created and added to the built-in administrators group
description: |
  'Identifies when a user account was created and then added to the builtin Administrators group in the same day.
  This should be monitored closely and all additions reviewed.'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
     - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T1078
query: |
  (union isfuzzy=true
      (SecurityEvent
      | where EventID == 4720
      | where AccountType == "User"
      | project CreatedUserTime = TimeGenerated, CreatedUserEventID = EventID, CreatedUserActivity = Activity, Computer = toupper(Computer), 
      CreatedUser = tolower(TargetAccount), CreatedUserAccountName = TargetUserName, CreatedUserDomainName = TargetDomainName, CreatedUserSid = TargetSid, 
      AccountUsedToCreateUser = SubjectAccount, CreatedByAccountName = SubjectUserName, CreatedByDomainName = SubjectDomainName, SidofAccountUsedToCreateUser = SubjectUserSid
      ),
      (WindowsEvent
      | where EventID == 4720
      | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
      | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
      | where AccountType == "User"
      | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
      | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
      | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
      | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
      | extend Activity="4720 - A user account was created."
      | extend TargetSid = tostring(EventData.TargetSid)
      | project CreatedUserTime = TimeGenerated, CreatedUserEventID = EventID, CreatedUserActivity = Activity, Computer = toupper(Computer), 
      CreatedUser = tolower(TargetAccount), CreatedUserAccountName = TargetUserName, CreatedUserDomainName = TargetDomainName, CreatedUserSid = TargetSid, 
      AccountUsedToCreateUser = SubjectAccount, CreatedByAccountName = SubjectUserName, CreatedByDomainName = SubjectDomainName, SidofAccountUsedToCreateUser = SubjectUserSid
      )
    )
  | join kind=inner
  (
    (union isfuzzy=true
      (SecurityEvent 
      | where AccountType == "User"
      // 4732 - A member was added to a security-enabled local group
      | where EventID == 4732
      // TargetSid is the builin Admins group: S-1-5-32-544
      | where TargetSid == "S-1-5-32-544"
      | project GroupAddTime = TimeGenerated, GroupAddEventID = EventID, GroupAddActivity = Activity, Computer = toupper(Computer), GroupName = tolower(TargetAccount), 
      GroupSid = TargetSid, AccountThatAddedUser = SubjectAccount, SIDofAccountThatAddedUser = SubjectUserSid, AddedByAccountName = SubjectUserName, AddedByDomainName = SubjectDomainName,
      CreatedUserSid = MemberSid
      ),
      (  WindowsEvent 
      // 4732 - A member was added to a security-enabled local group
      | where EventID == 4732 and EventData has "S-1-5-32-544"
      //TargetSid is the builin Admins group: S-1-5-32-544
      | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
      | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
      | where AccountType == "User"
      | extend TargetSid = tostring(EventData.TargetSid)
      | where TargetSid == "S-1-5-32-544"
      | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
      | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
      | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
      | extend Activity="4732 - A member was added to a security-enabled local group."
      | extend MemberSid = tostring(EventData.MemberSid)
      | project GroupAddTime = TimeGenerated, GroupAddEventID = EventID, GroupAddActivity = Activity, Computer = toupper(Computer), GroupName = tolower(TargetAccount), 
      GroupSid = TargetSid, AccountThatAddedUser = SubjectAccount, SIDofAccountThatAddedUser = SubjectUserSid, AddedByAccountName = SubjectUserName, AddedByDomainName = SubjectDomainName,
      CreatedUserSid = MemberSid
      )
    )
  )
  on CreatedUserSid
  //Create User first, then the add to the group.
  | project Computer, CreatedUserTime, CreatedUserEventID, CreatedUserActivity, CreatedUser, CreatedUserSid, CreatedUserAccountName, CreatedUserDomainName,
  GroupAddTime, GroupAddEventID, GroupAddActivity, GroupName, GroupSid,
  AccountUsedToCreateUser, SidofAccountUsedToCreateUser, CreatedByAccountName, CreatedByDomainName, 
  AccountThatAddedUser, SIDofAccountThatAddedUser, AddedByAccountName, AddedByDomainName
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountUsedToCreateUser
      - identifier: Name
        columnName: CreatedByAccountName
      - identifier: NTDomain
        columnName: CreatedByDomainName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountThatAddedUser
      - identifier: Name
        columnName: AddedByAccountName
      - identifier: NTDomain
        columnName: AddedByDomainName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: CreatedUser
      - identifier: Name
        columnName: CreatedUserAccountName
      - identifier: NTDomain
        columnName: CreatedUserDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: CreatedUserSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - 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 - Others", "Identity" ]

Stages and Predicates

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 EventID == 4720
    | where AccountType == "User"
    | project CreatedUserTime = TimeGenerated, CreatedUserEventID = EventID, CreatedUserActivity = Activity, Computer = toupper(Computer), 
    CreatedUser = tolower(TargetAccount), CreatedUserAccountName = TargetUserName, CreatedUserDomainName = TargetDomainName, CreatedUserSid = TargetSid, 
    AccountUsedToCreateUser = SubjectAccount, CreatedByAccountName = SubjectUserName, CreatedByDomainName = SubjectDomainName, SidofAccountUsedToCreateUser = SubjectUserSid

Leg 2: WindowsEvent

WindowsEvent
    | where EventID == 4720
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType == "User"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | extend TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName) 
    | extend Activity="4720 - A user account was created."
    | extend TargetSid = tostring(EventData.TargetSid)
    | project CreatedUserTime = TimeGenerated, CreatedUserEventID = EventID, CreatedUserActivity = Activity, Computer = toupper(Computer), 
    CreatedUser = tolower(TargetAccount), CreatedUserAccountName = TargetUserName, CreatedUserDomainName = TargetDomainName, CreatedUserSid = TargetSid, 
    AccountUsedToCreateUser = SubjectAccount, CreatedByAccountName = SubjectUserName, CreatedByDomainName = SubjectDomainName, SidofAccountUsedToCreateUser = SubjectUserSid

Applied to the Combined Result

| join kind=inner
(
  (union isfuzzy=true
    (SecurityEvent 
    | where AccountType == "User"
    | where EventID == 4732
    | where TargetSid == "S-1-5-32-544"
    | project GroupAddTime = TimeGenerated, GroupAddEventID = EventID, GroupAddActivity = Activity, Computer = toupper(Computer), GroupName = tolower(TargetAccount), 
    GroupSid = TargetSid, AccountThatAddedUser = SubjectAccount, SIDofAccountThatAddedUser = SubjectUserSid, AddedByAccountName = SubjectUserName, AddedByDomainName = SubjectDomainName,
    CreatedUserSid = MemberSid
    ),
    (  WindowsEvent 
    | where EventID == 4732 and EventData has "S-1-5-32-544"
    | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
    | extend AccountType=case(EventData.SubjectUserName endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
    | where AccountType == "User"
    | extend TargetSid = tostring(EventData.TargetSid)
    | where TargetSid == "S-1-5-32-544"
    | extend SubjectAccount = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
    | extend SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName)
    | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
    | extend Activity="4732 - A member was added to a security-enabled local group."
    | extend MemberSid = tostring(EventData.MemberSid)
    | project GroupAddTime = TimeGenerated, GroupAddEventID = EventID, GroupAddActivity = Activity, Computer = toupper(Computer), GroupName = tolower(TargetAccount), 
    GroupSid = TargetSid, AccountThatAddedUser = SubjectAccount, SIDofAccountThatAddedUser = SubjectUserSid, AddedByAccountName = SubjectUserName, AddedByDomainName = SubjectDomainName,
    CreatedUserSid = MemberSid
    )
  )
)
on CreatedUserSid
| project Computer, CreatedUserTime, CreatedUserEventID, CreatedUserActivity, CreatedUser, CreatedUserSid, CreatedUserAccountName, CreatedUserDomainName,
GroupAddTime, GroupAddEventID, GroupAddActivity, GroupName, GroupSid,
AccountUsedToCreateUser, SidofAccountUsedToCreateUser, CreatedByAccountName, CreatedByDomainName, 
AccountThatAddedUser, SIDofAccountThatAddedUser, AddedByAccountName, AddedByDomainName
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountThatAddedUserproject
AccountUsedToCreateUserproject
AddedByAccountNameproject
AddedByDomainNameproject
Computerproject
CreatedByAccountNameproject
CreatedByDomainNameproject
CreatedUserproject
CreatedUserAccountNameproject
CreatedUserActivityproject
CreatedUserDomainNameproject
CreatedUserEventIDproject
CreatedUserSidproject
CreatedUserTimeproject
GroupAddActivityproject
GroupAddEventIDproject
GroupAddTimeproject
GroupNameproject
GroupSidproject
SIDofAccountThatAddedUserproject
SidofAccountUsedToCreateUserproject
HostNameextend
HostNameDomainextend

Pathlock TDnR - SE16N Direct Table Change Documents

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

Detects direct table data changes made via SE16N (Table Browser) in SAP, forwarded by Pathlock Threat Detection and Response. SE16N changes bypass normal application workflows and audit trails, making them a high-risk activity that could indicate data manipulation, log tampering, or unauthorized direct database modifications.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c55
name: Pathlock TDnR - SE16N Direct Table Change Documents
kind: Scheduled
description: >-
  Detects direct table data changes made via SE16N (Table Browser) in SAP, forwarded by Pathlock
  Threat Detection and Response. SE16N changes bypass normal application workflows and audit trails,
  making them a high-risk activity that could indicate data manipulation, log tampering,
  or unauthorized direct database modifications.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Exfiltration
relevantTechniques:
  - T1562
  - T1048
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SE16N_CHANGEDOCS"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "SE16N_CHANGEDOCS"

Stage 3: project

| project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
          Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
          MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedUserproject
Areaproject
Bnameproject
CentralTsproject
DataSourceproject
DestIpproject
Eventidproject
Hostnameproject
Instanceproject
LogLineproject
MessageV1project
MessageV2project
MessageV3project
MessageV4project
MsgIdproject
MsgNoproject
MsgTypeproject
Reportproject
SrcIpproject
Subidproject
Sysidproject
Tcodeproject
TimeGeneratedproject

Possible SignIn from Azure Backdoor

#
Severity
medium
Time window
1h
Group by
$left.DomainName, $right.UserDomain, DomainName, InitiatedBy
Source
github.com/Azure/Azure-Sentinel

Identifies when a user adds an unverified domain as an authentication method, followed by a sign-in from a user the newly added domain. Threat actors may add custom domains to create a backdoor to your tenant. It's important to monitor whenever custom domains are added to the tenant.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: fa00014c-c5f4-4715-8f5b-ba567e19e41e
name: Possible SignIn from Azure Backdoor
description: |
  'Identifies when a user adds an unverified domain as an authentication method, followed by a sign-in from a user the newly added domain. Threat actors may add custom domains to create a backdoor to your tenant. It's important to monitor whenever custom domains are added to the tenant.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
      - AuditLogs 
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  // Microsoft Entra ID  Backdoors: Identity Federation
  //Ref: https://www.inversecos.com/2021/11/how-to-detect-azure-active-directory.html
  AuditLogs
  | where OperationName == "Add unverified domain"
  | where Result == "success"
  | extend InitiatedBy = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)
  | extend DomainName = tostring(TargetResources[0].displayName)
  | summarize DomainAddedTime = min(TimeGenerated), ModifiedProperties = make_set(parse_json(TargetResources[0].modifiedProperties),1048576) by InitiatedBy, DomainName
  | join kind=inner (
  SigninLogs
  | where ResultType == "0"
  | extend UserDomain = tostring(parse_json(split(UserPrincipalName,"@",1)[0]))
  | summarize SignInTime = min(TimeGenerated)  by UserPrincipalName, IPAddress, tostring(LocationDetails),AppDisplayName,ResourceDisplayName,UserDomain
  ) on $left.DomainName == $right.UserDomain
  // Getting UserName and Domain
  | extend Name = split(UserPrincipalName,"@",0), Domain = split(UserPrincipalName,"@",1)
  | mv-expand Name,Domain
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: NTDomain
        columnName: UserDomain
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  AppDisplayName: AppDisplayName
  ResourceDisplayName: ResourceDisplayName
  DomainAdded: DomainName
  InitiatedBy: InitiatedBy
  ModifiedProperties: ModifiedProperties
  DomainAddedTime: DomainAddedTime
  SignInTime: SignInTime
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName == "Add unverified domain"

Stage 3: where

| where Result == "success"

Stage 4: extend

| extend InitiatedBy = tostring(parse_json(tostring(InitiatedBy.user)).userPrincipalName)

Stage 5: extend

| extend DomainName = tostring(TargetResources[0].displayName)

Stage 6: summarize

| summarize DomainAddedTime = min(TimeGenerated), ModifiedProperties = make_set(parse_json(TargetResources[0].modifiedProperties),1048576) by InitiatedBy, DomainName

Stage 7: join

| join kind=inner (
SigninLogs
| where ResultType == "0"
| extend UserDomain = tostring(parse_json(split(UserPrincipalName,"@",1)[0]))
| summarize SignInTime = min(TimeGenerated)  by UserPrincipalName, IPAddress, tostring(LocationDetails),AppDisplayName,ResourceDisplayName,UserDomain
) on $left.DomainName == $right.UserDomain

Stage 8: extend

extend Domain, Name

Stage 9: mv-expand

mv-expand Name

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DomainAddedTimesummarize
DomainNamesummarize
InitiatedBysummarize
ModifiedPropertiessummarize
Domainextend
Nameextend

PRODAFT USTA - Compromised credential used in successful sign-in

#
Status
available
Severity
high
Time window
14d
Group by
MatchName
Source
github.com/Azure/Azure-Sentinel

Correlates corporate credentials exposed in PRODAFT USTA Account Takeover Prevention tickets with successful Microsoft Entra ID sign-ins for the same user. A successful sign-in by an account whose credentials are known to be compromised is a strong signal of account takeover. Requires the Microsoft Entra ID (SigninLogs) connector. Username matching is best-effort (case-insensitive) because USTA may record either an email or a bare username.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 5da571b5-54f6-42c2-8c3e-27edd987ceec
name: PRODAFT USTA - Compromised credential used in successful sign-in
description: |
  'Correlates corporate credentials exposed in PRODAFT USTA Account Takeover Prevention
  tickets with successful Microsoft Entra ID sign-ins for the same user. A successful
  sign-in by an account whose credentials are known to be compromised is a strong signal
  of account takeover. Requires the Microsoft Entra ID (SigninLogs) connector. Username
  matching is best-effort (case-insensitive) because USTA may record either an email or a
  bare username.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: PRODAFTUstaATPCCPDefinition
    dataTypes:
      - PRODAFTUstaCompromisedCredentials_CL
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1078
  - T1555
query: |
  let lookback = 14d;
  let detection = 1h;
  let compromised =
      PRODAFTUstaCompromisedCredentials
      | where Created > ago(lookback)
      | where IsCorporate == true
      | where isnotempty(Username)
      | extend MatchName = tolower(Username)
      | summarize arg_max(TimeGenerated, TicketId, CompanyName, InfectionDate, Source) by MatchName;
  SigninLogs
  | where TimeGenerated > ago(detection)
  | where ResultType == 0
  | where isnotempty(UserPrincipalName)
  | extend MatchName = tolower(UserPrincipalName)
  | join kind=inner compromised on MatchName
  | project
      TimeGenerated,
      UserPrincipalName,
      IPAddress,
      Location,
      AppDisplayName,
      ClientAppUsed,
      TicketId,
      CompanyName,
      Source,
      InfectionDate
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  Ticket: TicketId
  Company: CompanyName
  Source: Source
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1d
    matchingMethod: Selected
    groupByEntities:
      - Account
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: compromised used in Stage 6

let compromised = PRODAFTUstaCompromisedCredentials
    | where Created > ago(lookback)
    | where IsCorporate == true
    | where isnotempty(Username)
    | extend MatchName = tolower(Username)
    | summarize arg_max(TimeGenerated, TicketId, CompanyName, InfectionDate, Source) by MatchName;

Stage 1: source

SigninLogs

Stage 2: where

| where TimeGenerated > ago(detection)

Stage 3: where

| where ResultType == 0

Stage 4: where

| where isnotempty(UserPrincipalName)

Stage 5: extend

| extend MatchName = tolower(UserPrincipalName)

Stage 6: join

| join kind=inner compromised on MatchName

Stage 7: project

project AppDisplayName, ClientAppUsed, CompanyName, IPAddress, InfectionDate, Location, Source, TicketId, TimeGenerated, UserPrincipalName

Stage 8: summarize aggregation inside the join branch

summarize by MatchName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MatchNamesummarize

Qakbot Discovery Activies

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

This query searches for injected processes launching discovery activity. Qakbot has been observed leading to ransomware in numerous instances. It looks for discovery commands such as net.exe, whoami.exe, nslookup.exe, netstat.exe, arp.exe, and ping.exe.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: ba9db6b2-3d05-42ae-8aee-3a15bbe29f27
name: Qakbot Discovery Activies
description: |
  This query searches for injected processes launching discovery activity. Qakbot has been observed leading to ransomware in numerous instances. It looks for discovery commands such as net.exe, whoami.exe, nslookup.exe, netstat.exe, arp.exe, and ping.exe.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Discovery
  - Execution
relevantTechniques:
  - T1140
  - T1010
  - T1059
tags:
  - Ransomware
  - Qakbot
  - Discovery
query: |
  DeviceProcessEvents 
  | where InitiatingProcessFileName in~('mobsync.exe','explorer.exe')
  | where (FileName =~ 'net.exe' and InitiatingProcessCommandLine has_all('view','/all'))
       or (FileName =~ 'whoami.exe' and InitiatingProcessCommandLine has '/all')
       or (FileName =~ 'nslookup.exe' and InitiatingProcessCommandLine has_all('querytype=ALL','timeout=10'))
       or (FileName =~ 'netstat.exe' and InitiatingProcessCommandLine has '-nao')
       or (FileName =~ 'arp.exe' and InitiatingProcessCommandLine has '-a')
       or (FileName =~ 'ping.exe' and InitiatingProcessCommandLine has '-t' and InitiatingProcessCommandLine endswith '127.0.0.1')
  | summarize DiscoveryCommands = dcount(InitiatingProcessCommandLine), make_set(InitiatingProcessFileName), make_set(FileName), make_set(InitiatingProcessCommandLine) by DeviceId, DeviceName, bin(TimeGenerated, 5m)   
  | where DiscoveryCommands >= 3
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:  
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where InitiatingProcessFileName in~('mobsync.exe','explorer.exe')

Stage 3: where

| where (FileName =~ 'net.exe' and InitiatingProcessCommandLine has_all('view','/all'))
     or (FileName =~ 'whoami.exe' and InitiatingProcessCommandLine has '/all')
     or (FileName =~ 'nslookup.exe' and InitiatingProcessCommandLine has_all('querytype=ALL','timeout=10'))
     or (FileName =~ 'netstat.exe' and InitiatingProcessCommandLine has '-nao')
     or (FileName =~ 'arp.exe' and InitiatingProcessCommandLine has '-a')
     or (FileName =~ 'ping.exe' and InitiatingProcessCommandLine has '-t' and InitiatingProcessCommandLine endswith '127.0.0.1')

Stage 4: summarize

| summarize DiscoveryCommands = dcount(InitiatingProcessCommandLine), make_set(InitiatingProcessFileName), make_set(FileName), make_set(InitiatingProcessCommandLine) by DeviceId, DeviceName, bin(TimeGenerated, 5m)

Stage 5: where

| where DiscoveryCommands >= 3

Stage 6: extend

| extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 7: extend

| extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DiscoveryCommandsge
  • 3 transforms: number
field:"DiscoveryCommands" kind:ge value:"3"
FileNameeq
  • arp.exe corpus 10 (elastic 8, splunk 1, kusto 1)
  • net.exe corpus 30 (elastic 27, splunk 2, kusto 1)
  • netstat.exe corpus 10 (elastic 8, splunk 1, kusto 1)
  • nslookup.exe corpus 7 (elastic 3, splunk 3, kusto 1)
  • ping.exe corpus 12 (elastic 8, splunk 2, kusto 2)
  • whoami.exe corpus 14 (elastic 11, splunk 2, kusto 1)
field:"process_basename" kind:eq
InitiatingProcessCommandLineends_with
  • 127.0.0.1
field:"ParentCommandLine" kind:ends_with value:"127.0.0.1"
InitiatingProcessCommandLinematch
  • -a transforms: term
  • -nao transforms: term
  • -t transforms: term
  • /all transforms: term
  • querytype=ALL transforms: term
  • timeout=10 transforms: term
  • view transforms: term
field:"ParentCommandLine" kind:match
InitiatingProcessFileNamein
  • explorer.exe corpus 2 (splunk 1, kusto 1)
  • mobsync.exe
field:"parent_process_basename" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceIdsummarize
DeviceNamesummarize
DiscoveryCommandssummarize
HostNameextend
DnsDomainextend

Tenant Quota Deleted

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

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

Rule Body

id: 16183d97-c348-4f72-a943-27e80c9dfbd5
name: Tenant Quota Deleted
description: Detects when a tenant quota 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 == 24170\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 == 24170

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

TIE Golden Ticket

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

'Searches for Golden Ticket attacks.'

MITRE ATT&CK Coverage

Rule Body

id: 216e12dd-165a-4537-b241-32e1bd3330c7
name: TIE Golden Ticket
description: |
  'Searches for Golden Ticket attacks.'
severity: High
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1558.001
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  afad_parser
    | where MessageType == 2 and Codename == "Golden Ticket"
    | 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 == "Golden Ticket"

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
  • Golden Ticket corpus 3 (kusto 3)
field:"Codename" kind:eq value:"Golden Ticket"
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

UniFi Site Manager: System log shipping disabled

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

Identifies when a UniFi host transitions cloudSystemLogState from enabled to disabled. This defense-evasion signal may indicate an attacker silencing audit trails.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: f32950bc-6553-4c03-2686-a9c29ef318e8
name: "UniFi Site Manager: System log shipping disabled"
description: |
  Identifies when a UniFi host transitions cloudSystemLogState from enabled to disabled. This defense-evasion signal may indicate an attacker silencing audit trails.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Hosts_CL
queryFrequency: 15m
queryPeriod: 45m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
subTechniques:
  - T1562.006
  - T1562.008
query: |
  let prev = Unifi_SiteManager_Hosts_CL
            | where TimeGenerated between (ago(30m) .. ago(15m))
            | summarize arg_max(TimeGenerated, *) by Id
            | project id_s = Id, prevState = tostring(ReportedState.cloudSystemLogState);
        Unifi_SiteManager_Hosts_CL
        | where TimeGenerated > ago(15m)
        | summarize arg_max(TimeGenerated, *) by Id
        | extend id_s = Id,
                 SiteName = tostring(ReportedState.name),
                 currentState = tostring(ReportedState.cloudSystemLogState)
        | join kind=inner prev on id_s
        | where currentState == 'disabled' and prevState == 'enabled'
        | extend Activity = strcat('Cloud system log shipping was disabled on host ', SiteName, ' (', prevState, ' -> ', currentState, ')')
        | project TimeGenerated, Id, SiteName, Activity, prevState, currentState
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Id
      - identifier: DnsDomain
        columnName: SiteName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT12H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Let Binding: prev used in Stage 5

let prev = Unifi_SiteManager_Hosts_CL
          | where TimeGenerated between (ago(30m) .. ago(15m))
          | summarize arg_max(TimeGenerated, *) by Id
          | project id_s = Id, prevState = tostring(ReportedState.cloudSystemLogState);

Stage 1: source

Unifi_SiteManager_Hosts_CL

Stage 2: where

| where TimeGenerated > ago(15m)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by Id

Stage 4: extend

| extend id_s = Id,
               SiteName = tostring(ReportedState.name),
               currentState = tostring(ReportedState.cloudSystemLogState)

Stage 5: join

| join kind=inner prev on id_s

Stage 6: where

where currentState == "disabled" and prevState == "enabled"

Stage 7: extend

extend Activity

Stage 8: project

project Activity, Id, SiteName, TimeGenerated, currentState, prevState

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activityproject
Idproject
SiteNameproject
TimeGeneratedproject
currentStateproject
prevStateproject

Back to all Kusto rules