Detection rules › Kusto

Kusto rules: 0q

1Password - Potential insider privilege escalation via group

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

This will alert when an actor grants, or updates their own permissions via a group. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Telemetry Coverage

Rule Body

id: 398a1cf1-f56f-4700-912c-9bf4c8409ebc
name: 1Password - Potential insider privilege escalation via group
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when an actor grants, or updates their own permissions via a group. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.

  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078
query: |-
  OnePasswordEventLogs_CL
  | where log_source == "auditevents"
  | where action has_any("join", "role")
  | where object_type == "gm"
  | where tostring(actor_details.email) == tostring(aux_details.email)
  | extend
      TargetUsername = aux_details.email
      , ActorUsername = actor_details.email
      , SrcIpAddr = session.ip
      , GroupRole = case(
          aux_info == "R", "Group member"
          , aux_info == "A", "Group manager"
          , aux_info
      )
subTechniques:
- T1078.004
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: TargetUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where log_source == "auditevents"

Stage 3: where

| where action has_any("join", "role")

Stage 4: where

| where object_type == "gm"

Stage 5: where

| where tostring(actor_details.email) == tostring(aux_details.email)

Stage 6: extend

| extend
    TargetUsername = aux_details.email
    , ActorUsername = actor_details.email
    , SrcIpAddr = session.ip
    , GroupRole = case(
        aux_info == "R", "Group member"
        , aux_info == "A", "Group manager"
        , aux_info
    )
GroupRole =
ifaux_info == "R""Group member"
elifaux_info == "A""Group manager"
elseaux_info

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actionmatch
  • join transforms: term
  • role transforms: term
field:"action" kind:match
log_sourceeq
  • auditevents corpus 12 (kusto 12)
field:"log_source" kind:eq value:"auditevents"
object_typeeq
  • gm corpus 2 (kusto 2)
field:"object_type" kind:eq value:"gm"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
GroupRoleextend
SrcIpAddrextend
TargetUsernameextend

Apache - Put suspicious file

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

'Detects PUT or POST of suspicious file'

MITRE ATT&CK Coverage

Rule Body

id: c5d69e46-3b00-11ec-8d3d-0242ac130003
name: Apache - Put suspicious file
description: |
  'Detects PUT or POST of suspicious file'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    datatypes:
      - ApacheHTTPServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1190
  - T1133
  - T1048
query: |
  ApacheHTTPServer
  | where HttpRequestMethod in~ ("POST", "PUT") 
  | extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
  | where isnotempty(File)
  | where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"
  | extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

ApacheHTTPServer

Stage 2: where

| where HttpRequestMethod in~ ("POST", "PUT")

Stage 3: extend

| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))

Stage 4: where

| where isnotempty(File)

Stage 5: where

| where File matches regex @"([a-zA-Z0-9-_]+\.)([a-zA-Z0-9-]+\.[a-zA-Z0-9-]+)"

Stage 6: extend

| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Fileis_not_null
  • (no value, null check)
field:"File" kind:is_not_null
Fileregex_match
  • ([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)
field:"File" kind:regex_match value:"([a-zA-Z0-9-_]+.)([a-zA-Z0-9-]+.[a-zA-Z0-9-]+)"
HttpRequestMethodin
  • POST
  • PUT
field:"HttpRequestMethod" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fileextend
FileCustomEntityextend
UrlCustomEntityextend

AWSCloudTrail - Monitor AWS Credential abuse or hijacking

#
Status
available
Severity
low
Time window
1d
Group by
AWSRegion, AccountName, AccountUPNSuffix, AdditionalEventData, EventName, EventSource, EventTypeName, RecipientAccountId, ResponseElements, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityType, UserIdentityUserName
Source
github.com/Azure/Azure-Sentinel

Identifies AWS STS GetCallerIdentity calls made by assumed-role sessions. An attacker who has assumed the role of a legitimate account can call the GetCallerIdentity function to determine what account they are using. A legitimate user using legitimate credentials would not need to call GetCallerIdentity since they should already know what account they are using. More Information: https://duo.com/decipher/trailblazer-hunts-compromised-credentials-in-aws AWS STS GetCallerIdentity API: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 32555639-b639-4c2b-afda-c0ae0abefa55
name: AWSCloudTrail - Monitor AWS Credential abuse or hijacking
description: |
  Identifies AWS STS `GetCallerIdentity` calls made by assumed-role sessions.
  An attacker who has assumed the role of a legitimate account can call the GetCallerIdentity function to determine what account they are using.
  A legitimate user using legitimate credentials would not need to call GetCallerIdentity since they should already know what account they are using.
  More Information: https://duo.com/decipher/trailblazer-hunts-compromised-credentials-in-aws 
  AWS STS GetCallerIdentity API: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1580
query: |
  AWSCloudTrail
  | where EventName =~ "GetCallerIdentity" and UserIdentityType =~ "AssumedRole"
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case(UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SourceIpAddress, EventName, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid,
    UserAgent, UserIdentityUserName, SessionMfaAuthenticated, AWSRegion, EventSource, AdditionalEventData, ResponseElements
  | extend timestamp = StartTime
  | sort by EndTime desc nulls last
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  AWSRegion: AWSRegion
  EventName: EventName
  EventSource: EventSource
  EventType: EventTypeName
alertDetailsOverride:
  alertDisplayNameFormat: "AWS credential hijack indicator from {{AccountName}}"
  alertDescriptionFormat: "GetCallerIdentity invoked by {{AccountName}} from {{SourceIpAddress}} in {{AWSRegion}}."
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName =~ "GetCallerIdentity" and UserIdentityType =~ "AssumedRole"

Stage 3: extend (4 consecutive steps)

| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case(UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
  AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")

Stage 4: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by SourceIpAddress, EventName, EventTypeName, UserIdentityType, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, UserIdentityPrincipalid,
  UserAgent, UserIdentityUserName, SessionMfaAuthenticated, AWSRegion, EventSource, AdditionalEventData, ResponseElements

Stage 5: extend

| extend timestamp = StartTime

Stage 6: sort

| sort by EndTime desc nulls last

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventNameeq
  • GetCallerIdentity corpus 4 (sigma 3, kusto 1)
field:"aws::eventName" kind:eq value:"GetCallerIdentity"
UserIdentityTypeeq
  • AssumedRole corpus 8 (elastic 6, sigma 1, kusto 1)
field:"aws::userIdentity.type" kind:eq value:"AssumedRole"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
AdditionalEventDatasummarize
EndTimesummarize
EventNamesummarize
EventSourcesummarize
EventTypeNamesummarize
RecipientAccountIdsummarize
ResponseElementssummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimesummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityTypesummarize
UserIdentityUserNamesummarize
timestampextend

BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero Service Principal

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero Service Principal

Rule Body

id: 9fe3e89b-fd57-4df1-a6e7-8a875966dada
name: BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier Zero
  Service Principal
description: BloodHound Attack Path Finding - Non Tier Zero Resource Assigned to Tier
  Zero Service Principal
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: "BHEAttackPathsData_CL \n| where Finding == \"AzureNonT0ManagedIdentityAssignment\""
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "AzureNonT0ManagedIdentityAssignment"

Indicators

These rows show field, operator, and value matches.

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

CYFIRMA - Attack Surface - Malicious Domain/IP Reputation High Rule

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. The IP has been previously associated with hacking activity and web application attacks. Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."

MITRE ATT&CK Coverage

Rule Body

id: 7ff6f6d7-9672-4567-99fc-cb8a58c3bce7
name: CYFIRMA - Attack Surface - Malicious Domain/IP Reputation High Rule
description: |
  "This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. 
  The IP has been previously associated with hacking activity and web application attacks. 
  Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaAttackSurfaceAlertsConnector
    dataTypes:
      - CyfirmaASDomainIPReputationAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - CommandAndControl
  - Reconnaissance
  - Impact
  - DefenseEvasion
  - Exfiltration
relevantTechniques:
  - T1566.002
  - T1071.001
  - T1090.002
  - T1595.002
  - T1036.005
  - T1499
  - T1041
query: |
  // High Severity - Malicious Domain/IP Reputation Detected
  let timeFrame = 5m;
  CyfirmaASDomainIPReputationAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      Domain=sub_domain,
      TopDomain=top_domain,
      NetworkIP=ip,
      AlertUID=alert_uid,
      UID=uid,
      Categories=categories,
      IPversion=ip_version,
      ISP=isp,
      ThreatActors=threat_actors,
      Country=country,
      LastUsersReported=last_users_reported,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Domain,
      TopDomain,
      RiskScore,
      FirstSeen,
      LastSeen,
      NetworkIP,
      AlertUID,
      UID,
      Categories,
      IPversion,
      ISP,
      ThreatActors,
      Country,
      LastUsersReported,
      ProviderName,
      ProductName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: TopDomain
      - identifier: DnsDomain
        columnName: Domain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: NetworkIP
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  LastUsersReported: LastUsersReported
  AlertUID: AlertUID
  UID: UID
  Categories: Categories
  IPversion: IPversion
  ThreatActors: ThreatActors
  Country: Country
  ISP: ISP
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Malicious Domain/IP Reputation Alert - Domain: {{Domain}}, IP: {{NetworkIP}} "
  alertDescriptionFormat: "CYFIRMA - High Severity Malicious Domain/IP Reputation Alert - Domain: {{Domain}}, IP: {{NetworkIP}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaASDomainIPReputationAlerts_CL

Stage 2: where

| where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    Domain=sub_domain,
    TopDomain=top_domain,
    NetworkIP=ip,
    AlertUID=alert_uid,
    UID=uid,
    Categories=categories,
    IPversion=ip_version,
    ISP=isp,
    ThreatActors=threat_actors,
    Country=country,
    LastUsersReported=last_users_reported,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Domain,
    TopDomain,
    RiskScore,
    FirstSeen,
    LastSeen,
    NetworkIP,
    AlertUID,
    UID,
    Categories,
    IPversion,
    ISP,
    ThreatActors,
    Country,
    LastUsersReported,
    ProviderName,
    ProductName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
Categoriesproject
Countryproject
Domainproject
FirstSeenproject
IPversionproject
ISPproject
LastSeenproject
LastUsersReportedproject
NetworkIPproject
ProductNameproject
ProviderNameproject
RiskScoreproject
ThreatActorsproject
TimeGeneratedproject
TopDomainproject
UIDproject

CYFIRMA - Data Breach and Web Monitoring - Dark Web Medium Rule

#

This is a third-party alert feed, not a detection over modeled telemetry. The vendor product raised the finding; this rule forwards it into the SIEM. It is searchable for reference but is excluded from the detection-rule browse and the ATT&CK coverage matrix.

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

"Detects critical alerts from CYFIRMA related to sensitive data or credentials leaked on dark web forums. These events often indicate unauthorized access or compromise of enterprise systems, cloud environments, or identity platforms. Immediate investigation is required to assess breach scope and initiate mitigation, including credential resets, access reviews, and threat actor tracking."

MITRE ATT&CK Coverage

Rule Body

id: c0afeda7-4832-49a6-8d03-a5d137d513b5
name: CYFIRMA - Data Breach and Web Monitoring - Dark Web Medium Rule
description: |
  "Detects critical alerts from CYFIRMA related to sensitive data or credentials leaked on dark web forums. 
  These events often indicate unauthorized access or compromise of enterprise systems, cloud environments, or identity platforms. 
  Immediate investigation is required to assess breach scope and initiate mitigation, including credential resets, access reviews, and threat actor tracking."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: CyfirmaDigitalRiskAlertsConnector
    dataTypes:
      - CyfirmaDBWMDarkWebAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - Collection
  - Exfiltration
  - Impact
relevantTechniques:
  - T1552.001
  - T1555.003
  - T1212
  - T1119
  - T1048
  - T1486
query: |
  // Medium severity - Data Breach and Web Monitoring - Dark Web
  let timeFrame = 5m;
  CyfirmaDBWMDarkWebAlerts_CL
  | where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=signature,
      Impact=impact,
      Recommendation='',
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT',
      AlertTitle=Alert_title
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      ProductName,
      ProviderName,
      AlertTitle
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  RiskScore: RiskScore
  Impact: Impact
  Description: Description
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  TimeGenerated: TimeGenerated
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Medium Severity Alert: Critical Data Exposure of Enterprise SSO on Dark Web- {{AlertTitle}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaDBWMDarkWebAlerts_CL

Stage 2: where

| where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())

Stage 3: extend

| extend
    Description=description,
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    AlertUID=alert_uid,
    UID=uid,
    AssetType=asset_type,
    AssetValue=signature,
    Impact=impact,
    Recommendation='',
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT',
    AlertTitle=Alert_title

Stage 4: project

| project
    TimeGenerated,
    Description,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    AssetType,
    AssetValue,
    Impact,
    ProductName,
    ProviderName,
    AlertTitle

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertTitleproject
AlertUIDproject
AssetTypeproject
AssetValueproject
Descriptionproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
RiskScoreproject
TimeGeneratedproject
UIDproject

CYFIRMA - High severity Trojan Network Indicators - Monitor Recommended 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 analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."

MITRE ATT&CK Coverage

Rule Body

id: 89fd02b8-3c21-492c-a8de-b3e728d39119
name: CYFIRMA - High severity Trojan Network Indicators - Monitor Recommended Rule
description: |
  "This analytics rule detects network-based indicators flagged by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. 
  These indicators may represent attacker-controlled endpoints used for persistence, data exfiltration, or command delivery to compromised systems."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  //Trojan Network Indicators - Monitor Recommended 
  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 has 'Trojan'
  | extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
  | extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
  | 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 
      extension_id = extensionKeyStr,
      ASN_Owner = props.asn_owner,
      ASN = props.asn,
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project
      IPv4,
      IPv6,
      URL,
      Domain,
      ThreatActors,
      RecommendedActions,
      Sources,
      Roles,
      Country,
      IPAbuse,
      name,
      Description,
      ConfidenceScore,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      SecurityVendors,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - Impact
  - Persistence
  - DefenseEvasion
  - CredentialAccess
  - CommandAndControl
  - Execution
  - InitialAccess
relevantTechniques:
  - T1496
  - T1053
  - T1555
  - T1090
  - T1071
  - T1204
  - T1189
  - T1053.005
  - T1555.003
  - T1071.001
  - T1204.001
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Trojan Network Indicators - Monitor Recommended Rule - {{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
  IPAbuse: IPAbuse
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv4
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv6
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
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 has 'Trojan'

Stage 3: extend (6 consecutive steps)

| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| 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 
    extension_id = extensionKeyStr,
    ASN_Owner = props.asn_owner,
    ASN = props.asn,
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 6: project

| project
    IPv4,
    IPv6,
    URL,
    Domain,
    ThreatActors,
    RecommendedActions,
    Sources,
    Roles,
    Country,
    IPAbuse,
    name,
    Description,
    ConfidenceScore,
    IndicatorID,
    created,
    modified,
    valid_from,
    Tags,
    ThreatType,
    TimeGenerated,
    SecurityVendors,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
patterncontainsfile:hashesexcludes:pattern field:"pattern" value:"file:hashes"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConfidenceScoreproject
Countryproject
Descriptionproject
Domainproject
IPAbuseproject
IPv4project
IPv6project
IndicatorIDproject
ProductNameproject
ProviderNameproject
RecommendedActionsproject
Rolesproject
SecurityVendorsproject
Sourcesproject
Tagsproject
ThreatActorsproject
ThreatTypeproject
TimeGeneratedproject
URLproject
createdproject
modifiedproject
nameproject
valid_fromproject

Dataverse - Export activity from terminated or notified employee

#
Status
available
Severity
medium
Time window
1d
Group by
$left.UserPrincipalName, $right.UserId, ClientIp, InstanceUrl, UserId, UserState
Source
github.com/Azure/Azure-Sentinel

This query identifies Dataverse export activity triggered by terminated, or employees about to leave the organization. This analytics rule uses the TerminatedEmployees watchlist template.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 0881b209-62c9-4b15-9f9a-e0c1d1b1eb7b
kind: Scheduled
name: Dataverse - Export activity from terminated or notified employee
description: This query identifies Dataverse export activity triggered by terminated,
  or employees about to leave the organization. This analytics rule uses the TerminatedEmployees
  watchlist template.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
  - T1048
query: |
  // Set a time period before employee terminatation date to search for export events
  let termination_watch_period = 7d;
  let query_frequency = 1h;
  let exportEvents = dynamic(['ExportToExcel', 'ExportPdfDocument', 'ExportWordDocument', 'ExecutePowerBISql']);
  MSBizAppsTerminatedEmployees
  | where (UserState =~ "Terminated") or (UserState =~ "Notified" and TerminationDate <= startofday(now()) + termination_watch_period)
  | join kind=inner (DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message in (exportEvents))
      on $left.UserPrincipalName == $right.UserId
  | summarize
      FirstEvent = min(TimeGenerated),
      LastEvent = max(TimeGenerated),
      Event = make_set(Message, 4)
      by UserId, InstanceUrl, ClientIp, UserState
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstEvent,
      LastEvent,
      UserId,
      ClientIp,
      UserState,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Export events detected from a terminated employee
    in {{InstanceUrl}} '
  alertDescriptionFormat: Export events where employee state found matching {{UserState}}
    found in {{InstanceUrl}}.
version: 3.2.0

Stages and Predicates

Parameters

let termination_watch_period = 7d;
let query_frequency = 1h;
let exportEvents = dynamic(['ExportToExcel', 'ExportPdfDocument', 'ExportWordDocument', 'ExecutePowerBISql']);

Stage 1: source

MSBizAppsTerminatedEmployees

Stage 2: where

| where (UserState =~ "Terminated") or (UserState =~ "Notified" and TerminationDate <= startofday(now()) + termination_watch_period)

Stage 3: join

| join kind=inner (DataverseActivity
    | where TimeGenerated >= ago(query_frequency)
    | where Message in (exportEvents))
    on $left.UserPrincipalName == $right.UserId

Stage 4: summarize

summarize Event, FirstEvent, LastEvent by UserId, InstanceUrl, ClientIp, UserState

Stage 5: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 6: project

project AccountName, ClientIp, CloudAppId, FirstEvent, InstanceUrl, LastEvent, UPNSuffix, UserId, UserState

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Messagein
  • ExecutePowerBISql
  • ExportPdfDocument
  • ExportToExcel
  • ExportWordDocument
field:"Message" kind:in
UserStateeq
  • Notified
  • Terminated
field:"UserState" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
FirstEventproject
InstanceUrlproject
LastEventproject
UPNSuffixproject
UserIdproject
UserStateproject

Dev-0270 WMIC Discovery

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

The query below identifies dllhost.exe using WMIC to discover additional hosts and associated domains in the environment.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rule Body

id: 6b652b4f-9810-4eec-9027-7aa88ce4db23
name: Dev-0270 WMIC  Discovery
description: | 
  'The query below identifies dllhost.exe using WMIC to discover additional hosts and associated domains in the environment.'
severity: High 
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
status: Available
tactics: 
  - Discovery
relevantTechniques:
  - T1482
tags:
  - Dev-0270
query: |
  (union isfuzzy=true
  (SecurityEvent
  | where EventID==4688
  | where CommandLine has "wmic computersystem get domain" and ParentProcessName has "dllhost.exe"
  | project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type
  ),
  (DeviceProcessEvents 
  | where ProcessCommandLine has "wmic computersystem get domain" and InitiatingProcessFileName =~ "dllhost.exe" and InitiatingProcessCommandLine has "dllhost.exe"
  | extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName
  )
  )
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.0.4
kind: Scheduled

Stages and Predicates

Union isfuzzy=true (2 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
| where EventID==4688
| where CommandLine has "wmic computersystem get domain" and ParentProcessName has "dllhost.exe"
| project TimeGenerated, Computer, Account, AccountDomain, ProcessName, ProcessNameFullPath = NewProcessName, EventID, Activity, CommandLine, EventSourceName, Type

Leg 2: DeviceProcessEvents

DeviceProcessEvents 
| where ProcessCommandLine has "wmic computersystem get domain" and InitiatingProcessFileName =~ "dllhost.exe" and InitiatingProcessCommandLine has "dllhost.exe"
| extend Account = strcat(InitiatingProcessAccountDomain, @'\', InitiatingProcessAccountName), Computer = DeviceName

Applied to the Combined Result

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountextend
AccountDomainproject
Activityproject
CommandLineproject
Computerextend
EventIDproject
EventSourceNameproject
ProcessNameproject
ProcessNameFullPathproject
TimeGeneratedproject
Typeproject
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

Four-Eyes Authorization Request Expired

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

Detects when a four-eyes authorization request is expired.

Rule Body

id: a641dbe1-9590-46f8-a6d7-b5f24db6e126
name: Four-Eyes Authorization Request Expired
description: Detects when a four-eyes authorization request is expired.
severity: Medium
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 == 42405\n| extend Operation =\
  \ extract(\"Operation=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"Operation\"] = Operation,\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 == 42405

Stage 3: extend

| extend Operation = extract("Operation=\"([^\"]*)\"", 1, SyslogMessage)

Stage 4: project

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

Gain Code Execution on ADFS Server via Remote WMI Execution

#
Severity
medium
Time window
7d
Group by
TargetLogonId
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution. In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21. If you do not have Sysmon data in your workspace this query will raise an error stating: Failed to resolve scalar expression named "[@Name]" For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/. The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details. - ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

Rule Body

id: 0bd65651-1404-438b-8f63-eecddcec87b4
name: Gain Code Execution on ADFS Server via Remote WMI Execution
description: |
   'This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution.
   In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21.
   If you do not have Sysmon data in your workspace this query will raise an error stating:
        Failed to resolve scalar expression named "[@Name]"
   For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/.
   The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details.
   - ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
tags:
  - Solorigate
  - NOBELIUM
query: |
  let timeframe = 1d;
  // Adjust for a longer timeframe for identifying ADFS Servers
  let lookback = 6d;
  // Identify ADFS Servers
  let ADFS_Servers = ( union isfuzzy=true
  ( Event
  | where TimeGenerated > ago(timeframe+lookback)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  | extend process = split(Image, '\\', -1)[-1]
  | where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  ),
  ( SecurityEvent
  | where TimeGenerated > ago(timeframe+lookback)
  | where EventID == 4688 and SubjectLogonId != "0x3e4"
  | where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  ),
  (WindowsEvent
  | where TimeGenerated > ago(timeframe+lookback)
  | where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
  | extend SubjectLogonId  = tostring(EventData.SubjectLogonId)
  | where SubjectLogonId != "0x3e4"
  | extend ProcessName  = tostring(EventData.ProcessName)
  | where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  )
  | distinct Computer);
  (union isfuzzy=true
  (
  SecurityEvent
  | where EventID == 4688
  | where TimeGenerated > ago(timeframe)
  | where Computer in~ (ADFS_Servers)
  | where ParentProcessName has 'wmiprvse.exe'
  // Looking for rundll32.exe is based on intel from the blog linked in the description
  // This can be commented out or altered to filter out known internal uses
  | where CommandLine has_any ('rundll32') 
  | project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
  // Search for recent logons to identify lateral movement
  | join kind= inner
  (SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4624 and LogonType == 3
  | where Account !endswith "$"
  | project TargetLogonId
  ) on TargetLogonId
  ),
  (
  WindowsEvent
  | where EventID == 4688
  | where TimeGenerated > ago(timeframe)
  | where Computer in~ (ADFS_Servers)
  | where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32') 
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | where ParentProcessName has 'wmiprvse.exe'
  // Looking for rundll32.exe is based on intel from the blog linked in the description
  // This can be commented out or altered to filter out known internal uses
  | extend CommandLine = tostring(EventData.CommandLine)
  | where CommandLine has_any ('rundll32') 
  | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend TargetLogonId = tostring(EventData.TargetLogonId)
  | project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
  // Search for recent logons to identify lateral movement
  | join kind= inner
  (WindowsEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4624 
  | extend  LogonType = tostring(EventData.LogonType)
  | where LogonType == 3
  | extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | where Account !endswith "$"
  | extend TargetLogonId = tostring(EventData.TargetLogonId)
  | project TargetLogonId
  ) on TargetLogonId
  ),
  (
  Event
  | where TimeGenerated > ago(timeframe)
  | where Source == "Microsoft-Windows-Sysmon"
  // Check for WMI Events
  | where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  | project TimeGenerated, EventType, Image, Computer, UserName
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])
  )
  )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserName
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let timeframe = 1d;
let lookback = 6d;

Let Binding: ADFS_Servers

let ADFS_Servers = ( union isfuzzy=true
( Event
| where TimeGenerated > ago(timeframe+lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
( SecurityEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and SubjectLogonId != "0x3e4"
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
(WindowsEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
| extend SubjectLogonId  = tostring(EventData.SubjectLogonId)
| where SubjectLogonId != "0x3e4"
| extend ProcessName  = tostring(EventData.ProcessName)
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
)
| distinct Computer);

Union isfuzzy=true (3 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where ParentProcessName has 'wmiprvse.exe'
| where CommandLine has_any ('rundll32') 
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
| join kind= inner
(SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 and LogonType == 3
| where Account !endswith "$"
| project TargetLogonId
) on TargetLogonId

Leg 2: WindowsEvent

WindowsEvent
| where EventID == 4688
| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
| where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32') 
| extend ParentProcessName = tostring(EventData.ParentProcessName)
| where ParentProcessName has 'wmiprvse.exe'
| extend CommandLine = tostring(EventData.CommandLine)
| where CommandLine has_any ('rundll32') 
| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
| join kind= inner
(WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 
| extend  LogonType = tostring(EventData.LogonType)
| where LogonType == 3
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| where Account !endswith "$"
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TargetLogonId
) on TargetLogonId

Leg 3: Event

Event
| where TimeGenerated > ago(timeframe)
| where Source == "Microsoft-Windows-Sysmon"
| where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| project TimeGenerated, EventType, Image, Computer, UserName
| extend timestamp = TimeGenerated
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Accountends_with$excludes:Account field:"Account" value:"$"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
EventTypeproject
Imageproject
TimeGeneratedproject
UserNameproject
timestampextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend

McAfee ePO - Update failed

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

Detects when update failed event occurs on agent.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Telemetry Coverage

Rule Body

id: 4f0c91c3-1690-48f0-b538-4282dd5417a4
name: McAfee ePO - Update failed
description: |
  'Detects when update failed event occurs on agent.'
severity: Medium
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
  - T1070
query: |
  McAfeeEPOEvent
  | where EventId in ('2402', '1119', '1123')
  | extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

McAfeeEPOEvent

Stage 2: where

| where EventId in ('2402', '1119', '1123')

Stage 3: extend

| extend IPCustomEntity = DvcIpAddr, HostCustomEntity = DvcHostname

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIdin
  • 1119
  • 1123
  • 2402
field:"EventId" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostCustomEntityextend
IPCustomEntityextend

NGINX - Request to sensitive files

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

'Detects request to sensitive files.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2141ef6c-d158-4d44-b739-b145a4c21947
name: NGINX - Request to sensitive files
description: |
  'Detects request to sensitive files.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - NGINX_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1189
query: |
  let forbidden_files = dynamic(['shadow', 'passwd', 'id_rsa']);
  NGINXHTTPServer
  | extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))
  | where File in (forbidden_files)
  | extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let forbidden_files = dynamic(['shadow', 'passwd', 'id_rsa']);

Stage 1: source

NGINXHTTPServer

Stage 2: extend

| extend File = extract(@"(.*\/)?(.*)", 2, tostring(UrlOriginal))

Stage 3: where

| where File in (forbidden_files)

Stage 4: extend

| extend FileCustomEntity = File, UrlCustomEntity = UrlOriginal

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Filein
  • id_rsa
  • passwd
  • shadow
field:"File" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
Fileextend
FileCustomEntityextend
UrlCustomEntityextend

Oracle - Multiple client errors from single IP

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

'Detects multiple client errors from one source in short timeframe'

MITRE ATT&CK Coverage

Rule Body

id: 41775080-5740-11ec-bf63-0242ac130002
name: Oracle - Multiple client errors from single IP
description: |
  'Detects multiple client errors from one source in short timeframe'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 100;
  OracleWebLogicServerEvent
  | where HttpStatusCode >= 400 and HttpStatusCode <= 499 
  | summarize MultipleClientErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)
  | where MultipleClientErrors > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 100;

Stage 1: source

OracleWebLogicServerEvent

Stage 2: where

| where HttpStatusCode >= 400 and HttpStatusCode <= 499

Stage 3: summarize

| summarize MultipleClientErrors = count() by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 4: where

| where MultipleClientErrors > threshold

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
MultipleClientErrorssummarize
SrcIpAddrsummarize
IPCustomEntityextend

Oracle - Put file and get file from same IP address

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

'Detects put or get files from one source in short timeframe'

MITRE ATT&CK Coverage

Rule Body

id: 033e98d2-5740-11ec-bf63-0242ac130002
name: Oracle - Put file and get file from same IP address
description: |
  'Detects put or get files from one source in short timeframe' 
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let p = OracleWebLogicServerEvent
  | where HttpRequestMethod in~ ('POST', 'PUT')
  | sort by EventStartTime asc
  | summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);
  OracleWebLogicServerEvent
  | where HttpRequestMethod =~ 'GET'
  | sort by EventStartTime asc
  | summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)
  | join kind=innerunique (p) on UrlOriginal, SrcIpAddr
  | extend second = datetime_diff('second',get_time,post_time)
  | where second between (1 .. 300)
  | project second, post_time, get_time, SrcIpAddr, UrlOriginal
  | extend IPCustomEntity = SrcIpAddr, UrlCustomEntity = tostring(UrlOriginal)
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: UrlCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: p used in Stage 5

let p = OracleWebLogicServerEvent
| where HttpRequestMethod in~ ('POST', 'PUT')
| sort by EventStartTime asc
| summarize post_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal);

Stage 1: source

OracleWebLogicServerEvent

Stage 2: where

| where HttpRequestMethod =~ 'GET'

Stage 3: sort

| sort by EventStartTime asc

Stage 4: summarize

| summarize get_time=min(EventStartTime) by SrcIpAddr, tostring(UrlOriginal)

Stage 5: join

| join kind=innerunique (p) on UrlOriginal, SrcIpAddr

Stage 6: extend

extend second

Stage 7: where

where second >= 1 and second <= 300

Stage 8: project

project SrcIpAddr, UrlOriginal, get_time, post_time, second

Stage 9: extend

extend IPCustomEntity, UrlCustomEntity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrproject
UrlOriginalproject
get_timeproject
post_timeproject
secondproject
IPCustomEntityextend
UrlCustomEntityextend

Protection Group Settings Updated

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

Detects when protection group settings are updated in Veeam Backup & Replication. This might indicate configuration changes that require review.

Rule Body

id: e9b9efa5-4e19-4716-840b-51ef2feeaad1
name: Protection Group Settings Updated
description: Detects when protection group settings are updated in Veeam Backup &
  Replication. This might indicate configuration changes that require review.
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 == 29110\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = 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 == 29110

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = 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
UserNameproject

Red Sift - New email with URL from previously unseen source

#
Status
available
Severity
medium
Time window
14d
Group by
EmailFrom, SrcIp
Source
github.com/Azure/Azure-Sentinel

'Detects email forensics events that contain one or more URLs where the sender is using a source IP address not seen in the previous 14 days, which may indicate suspicious infrastructure changes or phishing activity.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 6084dfd8-830b-4839-9a9c-5f08cc984729
name: Red Sift - New email with URL from previously unseen source
description: |
  'Detects email forensics events that contain one or more URLs where the sender is using a source IP address not seen in the previous 14 days, which may indicate suspicious infrastructure changes or phishing activity.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RedSiftPush
    dataTypes:
      - RedSiftEmailForensics_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let lookback = 14d;
  let recentWindow = 1h;
  let historicalSources = RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", "")),
      SrcIp = tostring(column_ifexists("SrcIp", ""))
  | where TimeGenerated between (ago(lookback) .. ago(recentWindow))
  | where isnotempty(EmailFrom) and isnotempty(SrcIp)
  | summarize by EmailFrom, SrcIp;
  RedSiftEmailForensics_CL
  | extend
      EmailFrom = tostring(column_ifexists("EmailFrom", "")),
      EmailSubject = tostring(column_ifexists("EmailSubject", "")),
      EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
      EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
      SrcIp = tostring(column_ifexists("SrcIp", "")),
      DstHostname = tostring(column_ifexists("DstHostname", "")),
      Severity = tostring(column_ifexists("Severity", "")),
      Message = tostring(column_ifexists("Message", "")),
      CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
      EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))
  | where TimeGenerated >= ago(recentWindow)
  | where isnotempty(EmailFrom) and isnotempty(SrcIp)
  | extend UrlCount = array_length(EmailUrls)
  | where UrlCount > 0
  | mv-apply Url = EmailUrls on (
      summarize UrlSet = make_set(tostring(Url.url_string), 50)
    )
  | extend UrlList = strcat_array(UrlSet, ", ")
  | join kind=leftanti (historicalSources) on EmailFrom, SrcIp
  | project
      TimeGenerated,
      EmailFrom,
      EmailSubject,
      EmailReturnPath,
      EmailMessageUid,
      SrcIp,
      DstHostname,
      UrlCount,
      UrlList,
      Severity,
      Message,
      CorrelationUid
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: EmailFrom
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DstHostname
customDetails:
  EmailSubject: EmailSubject
  ReturnPath: EmailReturnPath
  UrlCount: UrlCount
  UrlList: UrlList
  CorrelationUid: CorrelationUid
alertDetailsOverride:
  alertDisplayNameFormat: "RedSift - New URL-bearing email source for {{EmailFrom}}"
  alertDescriptionFormat: "Email from {{EmailFrom}} contains {{UrlCount}} URL(s) and originated from previously unseen source IP {{SrcIp}}."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByEntities:
      - Account
      - IP
    groupByCustomDetails:
      - EmailSubject
eventGroupingSettings:
  aggregationKind: AlertPerResult
suppressionEnabled: false
suppressionDuration: PT1H
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: historicalSources used in Stage 9

let historicalSources = RedSiftEmailForensics_CL
| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", "")),
    SrcIp = tostring(column_ifexists("SrcIp", ""))
| where TimeGenerated between (ago(lookback) .. ago(recentWindow))
| where isnotempty(EmailFrom) and isnotempty(SrcIp)
| summarize by EmailFrom, SrcIp;

Stage 1: source

RedSiftEmailForensics_CL

Stage 2: extend

| extend
    EmailFrom = tostring(column_ifexists("EmailFrom", "")),
    EmailSubject = tostring(column_ifexists("EmailSubject", "")),
    EmailReturnPath = tostring(column_ifexists("EmailReturnPath", "")),
    EmailMessageUid = tostring(column_ifexists("EmailMessageUid", "")),
    SrcIp = tostring(column_ifexists("SrcIp", "")),
    DstHostname = tostring(column_ifexists("DstHostname", "")),
    Severity = tostring(column_ifexists("Severity", "")),
    Message = tostring(column_ifexists("Message", "")),
    CorrelationUid = tostring(column_ifexists("CorrelationUid", "")),
    EmailUrls = todynamic(column_ifexists("EmailUrls", "[]"))

Stage 3: where

| where TimeGenerated >= ago(recentWindow)

Stage 4: where

| where isnotempty(EmailFrom) and isnotempty(SrcIp)

Stage 5: extend

| extend UrlCount = array_length(EmailUrls)

Stage 6: where

| where UrlCount > 0

Stage 7: kusto:mv-apply

| mv-apply Url = EmailUrls on (
    summarize UrlSet = make_set(tostring(Url.url_string), 50)
  )

Stage 8: extend

| extend UrlList = strcat_array(UrlSet, ", ")

Stage 9: join (negated)

join kind=leftanti (historicalSources) on EmailFrom, SrcIp

Stage 10: project

project CorrelationUid, DstHostname, EmailFrom, EmailMessageUid, EmailReturnPath, EmailSubject, Message, Severity, SrcIp, TimeGenerated, UrlCount, UrlList

Stage 11: summarize aggregation inside the mv-apply branch

summarize

Stage 12: summarize aggregation inside the join branch

summarize by EmailFrom, SrcIp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EmailFromis_not_null
  • (no value, null check)
field:"EmailFrom" kind:is_not_null
SrcIpis_not_null
  • (no value, null check)
field:"SrcIp" kind:is_not_null
UrlCountgt
  • 0 transforms: number
field:"UrlCount" kind:gt value:"0"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
EmailFromis_not_null(no value, null check)excludes:EmailFrom
SrcIpis_not_null(no value, null check)excludes:SrcIp

Output Fields

These fields are emitted when the rule matches.

FieldSource
EmailFromsummarize
SrcIpsummarize

Request for single resource on domain

#
Status
available
Severity
low
Time window
1d
Group by
Domain
Source
github.com/Azure/Azure-Sentinel

'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 4d500e6d-c984-43a3-9f39-7edec8dcc04d
name: Request for single resource on domain
description: |
  'This will look for connections to a domain where only a single file is requested, this is unusual as most modern web applications require additional recources. This type of activity is often assocaited with malware beaconing or tracking URL's delivered in emails. Developed for Zscaler but applicable to any outbound web logging.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1102
  - T1071
query: |
  let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
  //The number of URI's seen to be suspicious, higher = less likely to be suspicious
  let uriThreshold = 1;
  CommonSecurityLog
  // Only look at connections that were allowed through the web proxy
  | where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"
  // Only look where some data was exchanged.
  | where SentBytes > 0 and ReceivedBytes > 0
  // Extract the Domain
  | extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
  | extend GetData=iff(RequestURL == "?", 1, 0)
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain
  // Determine the number of URIs that have been visited for the domain
  | extend destinationURI = arraylength(list_RequestURL)
  | where destinationURI <= uriThreshold
  | where tostring(list_RequestURL) has_any(scriptExtensions)
  //Remove matches with referer
  | where max_RequestContext == ""
  //Keep requests where data was trasferred either in a GET with parameters or a POST
  | where set_RequestMethod in~ ("POST") or max_GetData == 1
  //Defeat email click tracking, may increase FN's while decreasing FP's
  | where list_RequestURL !has "click" and set_RequestMethod !has "GET"
  | mvexpand list_RequestURL, list_DestinationIP
  | extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)
  //Extend custom entitites for incidents
  | project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DestinationIP
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let scriptExtensions = dynamic([".php", ".aspx", ".asp", ".cfml"]);
let uriThreshold = 1;

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor =~ "Zscaler" and DeviceAction =~ "Allowed"

Stage 3: where

| where SentBytes > 0 and ReceivedBytes > 0

Stage 4: extend

| extend Domain = iff(countof(DestinationHostName,'.') >= 2, strcat(split(DestinationHostName,'.')[-2], '.',split(DestinationHostName,'.')[-1]), DestinationHostName)
Domain =
if/* macro: (countof(DestinationHostName, '.') >= 2) */strcat(split(DestinationHostName, '.')[(- 2)], '.', split(DestinationHostName, '.')[(- 1)])
elseDestinationHostName

Stage 5: extend

| extend GetData=iff(RequestURL == "?", 1, 0)
GetData =
ifRequestURL matches regex "."1
else0

Stage 6: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), makelist(RequestURL), makelist(DestinationIP), makelist(SourceIP), numOfConnections = count(), make_set(RequestMethod), max(GetData), max(RequestContext) by Domain

Stage 7: extend

| extend destinationURI = arraylength(list_RequestURL)

Stage 8: where

| where destinationURI <= uriThreshold

Stage 9: where

| where tostring(list_RequestURL) has_any(scriptExtensions)

Stage 10: where

| where max_RequestContext == ""

Stage 11: where

| where set_RequestMethod in~ ("POST") or max_GetData == 1

Stage 12: where

| where list_RequestURL !has "click" and set_RequestMethod !has "GET"

Stage 13: mv-expand

| mvexpand list_RequestURL, list_DestinationIP

Stage 14: extend

| extend RequestURL = tostring(list_RequestURL), DestinationIP = tostring(list_DestinationIP), ClientIP = tostring(list_SourceIP)

Stage 15: project-away

| project-away list_RequestURL, list_DestinationIP, list_SourceIP, destinationURI, Domain, StartTimeUtc, EndTimeUtc, max_GetData, max_RequestContext

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
numOfConnectionssummarize
ClientIPextend
DestinationIPextend
RequestURLextend

UniFi Site Manager: Controller Connection State Change

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

Identifies when a UniFi controller (Cloud Key, Dream Machine, etc.) experiences a connection state change, which may indicate a network outage, power loss, or device failure.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control
Impact

Rule Body

id: ac1efe0f-654e-264e-07be-c1b60e698343
name: "UniFi Site Manager: Controller Connection State Change"
description: |
  Identifies when a UniFi controller (Cloud Key, Dream Machine, etc.) experiences a connection state change, which may indicate a network outage, power loss, or device failure.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_Hosts_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - CommandAndControl
relevantTechniques:
  - T1489
  - T1071
subTechniques:
  - T1071.001
query: |
  // UniFi Controller Connection State Change Detection
  // Only fire when LastConnectionStateChange has changed since the prior 15-min cycle
  // to avoid 4-5 alerts per actual state change (rule runs every 15m, change visible for 1h).
  let prev = Unifi_SiteManager_Hosts_CL
      | where TimeGenerated between (ago(30m) .. ago(15m))
      | summarize arg_max(TimeGenerated, *) by Id
      | project id_s = Id, prevChange = todatetime(LastConnectionStateChange);
  Unifi_SiteManager_Hosts_CL
  | where TimeGenerated > ago(15m)
  | where isnotempty(LastConnectionStateChange)
  | summarize arg_max(TimeGenerated, *) by Id
  | extend
      ConnectionChange = todatetime(LastConnectionStateChange),
      id_s = Id
  | join kind=leftouter prev on id_s
  | where ConnectionChange != prevChange or isnull(prevChange)
  | where ConnectionChange > ago(30m)
  | extend
      HostTypeDisplay = case(
          HostType == "ucore", "UniFi OS Console",
          HostType == "uck", "Cloud Key",
          HostType == "uckp", "Cloud Key+",
          HostType == "udm", "Dream Machine",
          HostType == "udmpro", "Dream Machine Pro",
          HostType == "udmse", "Dream Machine SE",
          HostType == "self-hosted", "Self-Hosted",
          HostType
      )
  | project
      TimeGenerated,
      ConnectionChangeTime = ConnectionChange,
      PreviousChangeTime = prevChange,
      HostId = Id,
      HostType = HostType,
      HostTypeDisplay,
      IPAddress = IpAddress,
      IsBlocked = IsBlocked,
      IsOwner = Owner
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT4H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Let Binding: prev used in Stage 6

let prev = Unifi_SiteManager_Hosts_CL
    | where TimeGenerated between (ago(30m) .. ago(15m))
    | summarize arg_max(TimeGenerated, *) by Id
    | project id_s = Id, prevChange = todatetime(LastConnectionStateChange);

Stage 1: source

Unifi_SiteManager_Hosts_CL

Stage 2: where

| where TimeGenerated > ago(15m)

Stage 3: where

| where isnotempty(LastConnectionStateChange)

Stage 4: summarize

| summarize arg_max(TimeGenerated, *) by Id

Stage 5: extend

| extend
    ConnectionChange = todatetime(LastConnectionStateChange),
    id_s = Id

Stage 6: join

| join kind=leftouter prev on id_s

Stage 7: where

where (ConnectionChange != prevChange or isnull(prevChange))

Stage 8: where

where ConnectionChange > ago(1800s)

Stage 9: extend

extend HostTypeDisplay
HostTypeDisplay =
ifHostType == "ucore""UniFi OS Console"
elifHostType == "uck""Cloud Key"
elifHostType == "uckp""Cloud Key+"
elifHostType == "udm""Dream Machine"
elifHostType == "udmpro""Dream Machine Pro"
elifHostType == "udmse""Dream Machine SE"
elifHostType == "self-hosted""Self-Hosted"
elseHostType

Stage 10: project

project ConnectionChangeTime, HostId, HostType, HostTypeDisplay, IPAddress, IsBlocked, IsOwner, PreviousChangeTime, TimeGenerated

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
LastConnectionStateChangeis_not_null
  • (no value, null check)
field:"LastConnectionStateChange" kind:is_not_null
prevChangeis_null
  • (no value, null check)
field:"prevChange" kind:is_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
ConnectionChangeTimeproject
HostIdproject
HostTypeproject
HostTypeDisplayproject
IPAddressproject
IsBlockedproject
IsOwnerproject
PreviousChangeTimeproject
TimeGeneratedproject

UnPAC the hash

#
Group by
DedupEntity
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query looks for an attack that allows an attacker with a valid TGT token for a certain account, to obtain the NTLM hash for that account. Such an account may either be a user account or a machine account. The TGT can, for example, be obtained by authenticating with a certificate instead of with username and password.

Known False Positives

  • No false positives observed.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));
SecurityEvent
| where ingestion_time() >= ago(timeframe)
| where EventID == 4769
| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)
| where ServiceName =~ TargetUserName // Requirement for getting the NT hash with U2U. This makes the KDC encrypt the NT hash with the key in the TGT.
| where binary_and(ticketOptions, krbflags) == krbflags
| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e // Sorting is required to ensure make_list is deterministic.
    | summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity
// End de-duplication logic.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let RuleId = "0299";
let DedupFields = dynamic(["TimeGenerated"]);
let forwardable = binary_shift_left(1, 30);
let renewable = binary_shift_left(1, 23);
let renewable_ok = binary_shift_left(1, 4);
let enctik = binary_shift_left(1, 3);
let krbflags = binary_or(forwardable, binary_or(renewable, binary_or(renewable_ok, binary_or(enctik, 0))));

Stage 1: source

SecurityEvent

Stage 2: where

| where ingestion_time() >= ago(timeframe)

Stage 3: where

| where EventID == 4769

Stage 4: extend (4 consecutive steps)

| extend ticketOptions = toint(extract("<Data Name=\"TicketOptions\">(.*?)</Data>", 1, EventData))
| extend TargetUserName = extract("<Data Name=\"TargetUserName\">(.*?)@.*?</Data>", 1, EventData)
| extend TargetDomainName = extract("<Data Name=\"TargetDomainName\">(.*?)</Data>", 1, EventData)
| extend ServiceName = extract("<Data Name=\"ServiceName\">(.*?)</Data>", 1, EventData)

Stage 5: where

| where ServiceName =~ TargetUserName

Stage 6: where

| where binary_and(ticketOptions, krbflags) == krbflags

Stage 7: extend

| extend HostName=tostring(split(Computer,".")[0]),DnsDomain=iif(Computer contains ".", substring(Computer, indexof(Computer, ".") + 1, strlen(Computer)),"")

Stage 8: extend

| extend DedupFieldValues=pack_all()

Stage 9: kusto:mv-apply

| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e
    | summarize DedupValues=make_list(DedupValue)
)

Stage 10: extend

| extend DedupEntity=strcat_array(DedupValues, "|")

Stage 11: project-away

| project-away DedupFieldValues, DedupValues

Stage 12: join (negated)

join kind=leftanti (SecurityAlert) on DedupEntity

Stage 13: summarize aggregation inside the mv-apply branch

summarize

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4769 corpus 2 (elastic 1, kusto 1)
field:"EventID" kind:eq value:"4769"
ticketOptionseq
  • krbflags transforms: binary_and
field:"ticketOptions" kind:eq value:"krbflags"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AlertNamematch0299excludes:AlertName field:"AlertName" value:"0299"
ProviderNamematchASIexcludes:ProviderName field:"ProviderName" value:"ASI"

Back to all Kusto rules