Detection rules › Kusto

Kusto rules: 1c

[Entra ID] Privileged Role Assigned to a New User

#
Status
available
Severity
high
Time window
14d
Group by
OperationName, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

Detects when a privileged role is assigned to a new user account. This can indicate unauthorized elevation of privileges.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: b64ac0e2-a241-4b9a-ad3e-ae572630b295
name: "[Entra ID] Privileged Role Assigned to a New User"
version: 1.0.0
kind: Scheduled
description: |
  Detects when a privileged role is assigned to a new user account. This can indicate unauthorized elevation of privileges.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
  - DefenseEvasion
  - InitialAccess
relevantTechniques:
  - T1078.004
query: |
  // Define the start and end times based on input values
  let starttime = now() - 1h;
  let endtime = now();
  // Set a lookback period of 14 days
  let lookback = starttime - 14d;
  // Define a reusable function to query audit logs
  let awsFunc = (start: datetime, end: datetime) {
      AuditLogs
      | where TimeGenerated between (start .. end)
      | where Category =~ "RoleManagement"
      | where AADOperationType in ("Assign", "AssignEligibleRole")
      | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
      | mv-apply TargetResource = TargetResources on
          (
          where TargetResource.type in~ ("User", "ServicePrincipal")
          | extend
              Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
              props = TargetResource.modifiedProperties
          )
      | mv-apply Property = props on
          (
          where Property.displayName =~ "Role.DisplayName"
          | extend RoleName = trim('"', tostring(Property.newValue))
          )
      | where RoleName contains "Admin" and Result == "success"
  };
  // Query for audit events in the current day
  let EventInfo_CurrentDay = awsFunc(starttime, endtime);
  // Query for audit events in the historical period (lookback)
  let EventInfo_historical = awsFunc(lookback, starttime);
  // Find unseen events by performing a left anti-join
  let EventInfo_Unseen = (EventInfo_CurrentDay
      | join kind=leftanti(EventInfo_historical) on Target, RoleName, OperationName
      );
  // Extend and clean up the results
  EventInfo_Unseen
  | 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)
  // You can uncomment the lines below to filter out PIM activations
  | where Initiator != "MS-PIM"
  //| summarize StartTime=min(TimeGenerated), EndTime=min(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result
  // Project specific columns and split them for further analysis
  | project
      TimeGenerated,
      OperationName,
      RoleName,
      Target,
      Initiator,
      InitiatingUserPrincipalName,
      InitiatingAadUserId,
      InitiatingAppName,
      InitiatingAppServicePrincipalId,
      InitiatingIpAddress,
      Result
  | extend
      TargetName = tostring(split(Target, '@', 0)[0]),
      TargetUPNSuffix = tostring(split(Target, '@', 1)[0]),
      InitiatorName = tostring(split(InitiatingUserPrincipalName, '@', 0)[0]),
      InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName, '@', 1)[0])
  | extend
      Source_Network_IPLocation = "",
      ActivityType=OperationName
  | project
          Alert_Category_en = "Entra ID",
      Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
      Alert_Name_en = "Privileged Role Assigned to a New User",
      Alert_Description_en = strcat(
                             "At Taiwan time: ",
                             format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
                             "in the Microsoft Entra ID tenant",
                             "operator: ",
                             iff(isnotempty(Initiator), Initiator, InitiatingAppName),
                             ", from IP: ",
                             InitiatingIpAddress,
                             ", assigned privileged role  ",
                             tostring(RoleName),
                             "  to  ",
                             iff(isnotempty(Target), Target, "<NoTargetUser>"),
                             ". This permission change behavior has not appeared for this user recently (within 14 days)."
                         ),
      Alert_TriageStep_en = strcat(
                            "1. Confirm whether the privileged role assignment was performed through an Entra ID PIM-approved change process.",
                            "2. Confirm with the operator: ",
                            iff(isnotempty(Initiator), Initiator, "<UnknownInitiator>"),
                            "  whether this was performed by the account owner.",
                            "3. Check whether the source IP and geolocation are abnormal or not a company public IP."
                        ),
      Alert_Containment_en = strcat(
                             "1. If determined to be an unauthorized role assignment, immediately revoke the target  ",
                             iff(isnotempty(Target), Target, "<NoTargetUser>"),
                             " 's privileged role (",
                             tostring(RoleName),
                             "), and restore the state to before the change.",
                             "2. Immediately restrict the source identity (user or application) that performed the role assignment, revoke sign-in tokens, and force password and MFA reset if necessary.  ",
                             "3. If the source is an application (",
                             iff(isnotempty(InitiatingAppName), InitiatingAppName, "<NoServicePrincipal>"),
                             "), immediately disable the Service Principal and revoke high-privilege API consent and credentials/secrets.  "
                         ),
      Alert_Remediation_en = strcat(
                             "1. Allow privileged role access only through Entra ID PIM.",
                             "2. Enforce Conditional Access and MFA for role management and PIM operations, allowing them only from managed devices and named locations.",
                             "3. Establish real-time alerts and automated response (SOAR) for privileged role changes to detect abnormal role assignments immediately. "
                         ),
              Alert_Time_TW = datetime_utc_to_local(TimeGenerated, "Asia/Taipei"),
      Alert_Time_UTC0 = TimeGenerated,
      Event_Action = ActivityType,
      Event_Status = Result,
      //Event_Description = ActivityDisplayName,
      Source_Identity_FullName = iff(isnotempty(Initiator), Initiator, InitiatingAppName),
      //Source_Identity_ID = ActorID,
      Source_Identity_Type = iff(isnotempty(Initiator), "User", "Service"),
      Source_Network_IPAddress = InitiatingIpAddress,
      Source_Network_IPLocation = Source_Network_IPLocation,
      //Source_Resource_Name = InitiatingAppName,
      Target_Identity_FullName = Target,
      Target_Identity_DomainType = iff(Target contains "EXT", 'External', 'Internal'),
      Target_Identity_Type = "User",
      Target_Resource_ID = "",
      Target_Resource_Type = "Entra ID"
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Source_Identity_FullName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target_Identity_FullName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Source_Network_IPAddress

Stages and Predicates

Parameters

let starttime = now() - 1h;
let endtime = now();
let lookback = starttime - 14d;
let EventInfo_CurrentDay = awsFunc(starttime, endtime);
let EventInfo_historical = awsFunc(lookback, starttime);

let EventInfo_Unseen is inlined into the numbered stages below.

Let Binding: awsFunc used in Stages 1, 9

let awsFunc = (start: datetime, end: datetime) {
    AuditLogs
    | where TimeGenerated between (start .. end)
    | where Category =~ "RoleManagement"
    | where AADOperationType in ("Assign", "AssignEligibleRole")
    | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
    | mv-apply TargetResource = TargetResources on
        (
        where TargetResource.type in~ ("User", "ServicePrincipal")
        | extend
            Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
            props = TargetResource.modifiedProperties
        )
    | mv-apply Property = props on
        (
        where Property.displayName =~ "Role.DisplayName"
        | extend RoleName = trim('"', tostring(Property.newValue))
        )
    | where RoleName contains "Admin" and Result == "success"
};

Stage 1: source

AuditLogs

Stage 2: where

where TimeGenerated >= "start" and TimeGenerated <= "end"

Stage 3: where

where Category =~ "RoleManagement"

Stage 4: where

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

Stage 5: where

where (ActivityDisplayName has "Add eligible member to role" or ActivityDisplayName has "Add member to role")

Stage 6: kusto:mv-apply

kusto:mv-apply TargetResource.type in~ ("ServicePrincipal", "User")

Stage 7: kusto:mv-apply

kusto:mv-apply Property.displayName =~ "Role.DisplayName"

Stage 8: where

where Result == "success" and RoleName contains "Admin"

Stage 9: join (negated)

join kind=leftanti (EventInfo_historical) on Target, RoleName, OperationName

Stage 10: extend (6 consecutive steps)

extend InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator

Stage 11: where

where Initiator != "MS-PIM"

Stage 12: project

project InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, OperationName, Result, RoleName, Target, TimeGenerated

Stage 13: extend

extend InitiatorName, InitiatorUPNSuffix, TargetName, TargetUPNSuffix

Stage 14: extend

extend ActivityType, Source_Network_IPLocation

Stage 15: project

project Alert_Category_en, Alert_Containment_en, Alert_Description_en, Alert_Name_en, Alert_Remediation_en, Alert_SubCategory_en, Alert_Time_TW, Alert_Time_UTC0, Alert_TriageStep_en, Event_Action, Event_Status, Source_Identity_FullName, Source_Identity_Type, Source_Network_IPAddress, Source_Network_IPLocation, Target_Identity_DomainType, Target_Identity_FullName, Target_Identity_Type, Target_Resource_ID, Target_Resource_Type

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypein
  • Assign corpus 6 (kusto 6)
  • AssignEligibleRole corpus 5 (kusto 5)
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)
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
field:"Initiator" kind:ne value:"MS-PIM"
Property.displayNameeq
  • Role.DisplayName transforms: kusto_dynamic_path
field:"Property.displayName" kind:eq value:"Role.DisplayName"
Resulteq
  • success
field:"Result" kind:eq value:"success"
RoleNamecontains
  • Admin
field:"RoleName" kind:contains value:"Admin"
TargetResource.typein
  • ServicePrincipal transforms: kusto_dynamic_path
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:in
TimeGeneratedge
  • start
field:"TimeGenerated" kind:ge value:"start"
TimeGeneratedle
  • end
field:"TimeGenerated" kind:le value:"end"

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Alert_Category_enproject
Alert_Containment_enproject
Alert_Description_enproject
Alert_Name_enproject
Alert_Remediation_enproject
Alert_SubCategory_enproject
Alert_Time_TWproject
Alert_Time_UTC0project
Alert_TriageStep_enproject
Event_Actionproject
Event_Statusproject
Source_Identity_FullNameproject
Source_Identity_Typeproject
Source_Network_IPAddressproject
Source_Network_IPLocationproject
Target_Identity_DomainTypeproject
Target_Identity_FullNameproject
Target_Identity_Typeproject
Target_Resource_IDproject
Target_Resource_Typeproject

AWSCloudTrail - User IAM Enumeration

#
Status
available
Severity
medium
Time window
10m
Group by
UserIdentityAccountId, UserIdentityArn, UserIdentityPrincipalid
Source
github.com/Azure/Azure-Sentinel

Detects enumeration of IAM account configuration via repeated API calls to list roles, users, groups, and policies within a short time period. The threshold can be adjusted to reduce false positives from authorized cloud scanners such as Wiz.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: cfaaf0bc-16d1-48df-ac8b-9d901bbd516a
name: AWSCloudTrail - User IAM Enumeration
description: |
  Detects enumeration of IAM account configuration via repeated API calls to list roles, users, groups, and policies within a short time period. The threshold can be adjusted to reduce false positives from authorized cloud scanners such as Wiz.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1580
query: |
 let threshold = 50; 
 AWSCloudTrail
 | where EventName in ("ListAttachedRolePolicies","ListRoles","ListGroupsForUser","ListAttachedUserPolicies","ListAccessKeys","ListUsers")
 | summarize
     count(),
     make_set(AWSRegion),
     make_set(UserAgent),
     make_set(SourceIpAddress),
     make_set(ErrorCode),
     make_set(ErrorMessage),
     make_set(EventName)
     by bin(TimeGenerated, 10m), UserIdentityPrincipalid, UserIdentityArn, UserIdentityAccountId
 | where count_ > threshold
 | mv-expand set_SourceIpAddress
 | extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)
 | extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UpnSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: set_SourceIpAddress
customDetails:
  EventCount: count_
  AWSAccountId: UserIdentityAccountId
  PrincipalId: UserIdentityPrincipalid
alertDetailsOverride:
  alertDisplayNameFormat: IAM enumeration detected - {{count_}} API calls by {{UserIdentityPrincipalid}}
  alertDescriptionFormat: User {{UserIdentityPrincipalid}} performed {{count_}} IAM enumeration API calls within a 10-minute window from account {{UserIdentityAccountId}}.
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 50;

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in ("ListAttachedRolePolicies","ListRoles","ListGroupsForUser","ListAttachedUserPolicies","ListAccessKeys","ListUsers")

Stage 3: summarize

| summarize
    count(),
    make_set(AWSRegion),
    make_set(UserAgent),
    make_set(SourceIpAddress),
    make_set(ErrorCode),
    make_set(ErrorMessage),
    make_set(EventName)
    by bin(TimeGenerated, 10m), UserIdentityPrincipalid, UserIdentityArn, UserIdentityAccountId

Stage 4: where

| where count_ > threshold

Stage 5: mv-expand

| mv-expand set_SourceIpAddress

Stage 6: extend

| extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)

Stage 7: extend

| extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventNamein
  • ListAccessKeys
  • ListAttachedRolePolicies
  • ListAttachedUserPolicies
  • ListGroupsForUser corpus 2 (kusto 1, panther 1)
  • ListRoles
  • ListUsers
field:"aws::eventName" kind:in
count_gt
  • 50 transforms: number
field:"count_" kind:gt value:"50"

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdentityAccountIdsummarize
UserIdentityArnsummarize
UserIdentityPrincipalidsummarize
UserNameextend
Nameextend
UpnSuffixextend

Azure DevOps Audit Stream Disabled

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

'Azure DevOps allow for audit logs to be streamed to external storage solutions such as SIEM solutions. An attacker looking to hide malicious Azure DevOps activity from defenders may look to disable data streams before conducting activity and then re-enabling the stream after (so as not to raise data threshold-based alarms). Looking for disabled audit streams can identify this activity, and due to the nature of the action its unlikely to have a high false positive rate.'

MITRE ATT&CK Coverage

Rule Body

id: 4e8238bd-ff4f-4126-a9f6-09b3b6801b3d
name: Azure DevOps Audit Stream Disabled
description: |
  'Azure DevOps allow for audit logs to be streamed to external storage solutions such as SIEM solutions. An attacker looking to hide malicious Azure DevOps activity from defenders may look to disable data streams before conducting activity and then re-enabling the stream after (so as not to raise data threshold-based alarms). Looking for disabled audit streams can identify this activity, and due to the nature of the action its unlikely to have a high false positive rate.'
severity: High
status: Available
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.008
query: |
  ADOAuditLogs
  | where OperationName =~ "AuditLog.StreamDisabledByUser"
  | extend StreamType = tostring(Data.ConsumerType)
  | project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, StreamType
  | extend timestamp = TimeGenerated
  | extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorUPN  
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.5
kind: Scheduled

Stages and Predicates

Stage 1: source

ADOAuditLogs

Stage 2: where

| where OperationName =~ "AuditLog.StreamDisabledByUser"

Stage 3: extend

| extend StreamType = tostring(Data.ConsumerType)

Stage 4: project-reorder

| project-reorder TimeGenerated, Details, ActorUPN, IpAddress, UserAgent, StreamType

Stage 5: extend

| extend timestamp = TimeGenerated

Stage 6: extend

| extend AccountName = tostring(split(ActorUPN, "@")[0]), AccountUPNSuffix = tostring(split(ActorUPN, "@")[1])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
StreamTypeextend
timestampextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Get Certifcates on Tier Zero Key Vault

#

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 - Get Certifcates on Tier Zero Key Vault

Rule Body

id: 21d458a0-b0d5-47bb-9fd1-fdda31f3b7f1
name: BloodHound Attack Path Finding - Get Certifcates on Tier Zero Key Vault
description: BloodHound Attack Path Finding - Get Certifcates on Tier Zero Key Vault
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "AzureT0GetCerts"
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 == "AzureT0GetCerts"

Indicators

These rows show field, operator, and value matches.

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

BloodHound Attack Path Finding - Large Default Groups With Read GMSA Password Privileges

#

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

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

BloodHound Attack Path Finding - Large Default Groups With Read GMSA Password Privileges

Rule Body

id: 86eda318-a551-438c-b72a-3db0ba6bc27e
name: BloodHound Attack Path Finding - Large Default Groups With Read GMSA Password
  Privileges
description: BloodHound Attack Path Finding - Large Default Groups With Read GMSA
  Password Privileges
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "LargeDefaultGroupsReadGMSA"
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 == "LargeDefaultGroupsReadGMSA"

Indicators

These rows show field, operator, and value matches.

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

CYFIRMA - Attack Surface - Configuration 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 generated when CYFIRMA detects a critical misconfiguration in a public-facing asset or service. Such misconfigurations may include exposed admin interfaces, default credentials, open directory listings, or insecure protocols, which significantly increase the attack surface."

MITRE ATT&CK Coverage

Rule Body

id: 30206b45-75d2-4c6a-87c5-f0861c1f2870
name: CYFIRMA - Attack Surface - Configuration High Rule
description: |
  This alert is generated when CYFIRMA detects a critical misconfiguration in a public-facing asset or service. 
  Such misconfigurations may include exposed admin interfaces, default credentials, open directory listings, or insecure protocols, which significantly increase the attack surface."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaAttackSurfaceAlertsConnector
    dataTypes:
      - CyfirmaASConfigurationAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Discovery
  - Persistence
  - Execution
  - DefenseEvasion
  - CredentialAccess
  - Collection
  - Reconnaissance
relevantTechniques:
  - T1190
  - T1087
  - T1046
  - T1136
  - T1059
  - T1566
  - T1070
  - T1027
  - T1505
  - T1555
  - T1114
  - T1595
query: |
  // High Severity - Attack Surface - Misconfiguration Detected
  let timeFrame = 5m;
  CyfirmaASConfigurationAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      Domain=sub_domain,
      TopDomain=top_domain,
      NetworkIP=ip,
      AlertUID=alert_uid,
      UID=uid,
      Softwares=software,
      WebAppFirewall=web_app_firewall,
      ClickJackingDefence=click_jacking_defence,
      ContentSecurityPolicy=content_security_policy,
      CookieXssProtection=cookie_xss_protection,
      DataInjectionDefence=data_injection_defence,
      DomainStatus=domain_status,
      MissingEPPCodes=missing_epp_codes,
      SecureCookie=secure_cookie,
      SetCookieHttpsOnly=set_cookie_https_only,
      XFrameOptions=x_frame_options,
      X_XssProtection=x_xss_protection,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      Domain,
      TopDomain,
      RiskScore,
      FirstSeen,
      LastSeen,
      NetworkIP,
      AlertUID,
      UID,
      Softwares,
      WebAppFirewall,
      ClickJackingDefence,
      ContentSecurityPolicy,
      CookieXssProtection,
      DataInjectionDefence,
      DomainStatus,
      MissingEPPCodes,
      SecureCookie,
      SetCookieHttpsOnly,
      XFrameOptions,
      X_XssProtection,
      ProviderName,
      ProductName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: TopDomain
      - identifier: DnsDomain
        columnName: Domain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: NetworkIP
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  Softwares: Softwares
  WebAppFirewall: WebAppFirewall
  ClickJackingDefence: ClickJackingDefence
  SecurityPolicy: ContentSecurityPolicy
  CookieXssProtection: CookieXssProtection
  InjectionDefence: DataInjectionDefence
  DomainStatus: DomainStatus
  MissingEPPCodes: MissingEPPCodes
  SecureCookie: SecureCookie
  SetCookieHttpsOnly: SetCookieHttpsOnly
  XFrameOptions: XFrameOptions
  X_XssProtection: X_XssProtection
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Risk Misconfiguration Identified in Assets - Domain: {{Domain}} , IP: {{NetworkIP}} "
  alertDescriptionFormat: "CYFIRMA - High Risk Misconfiguration Identified in Assets - {{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
  

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaASConfigurationAlerts_CL

Stage 2: where

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

Stage 3: extend

| extend
    Description=description,
    FirstSeen=first_seen,
    LastSeen=last_seen,
    RiskScore=risk_score,
    Domain=sub_domain,
    TopDomain=top_domain,
    NetworkIP=ip,
    AlertUID=alert_uid,
    UID=uid,
    Softwares=software,
    WebAppFirewall=web_app_firewall,
    ClickJackingDefence=click_jacking_defence,
    ContentSecurityPolicy=content_security_policy,
    CookieXssProtection=cookie_xss_protection,
    DataInjectionDefence=data_injection_defence,
    DomainStatus=domain_status,
    MissingEPPCodes=missing_epp_codes,
    SecureCookie=secure_cookie,
    SetCookieHttpsOnly=set_cookie_https_only,
    XFrameOptions=x_frame_options,
    X_XssProtection=x_xss_protection,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Description,
    Domain,
    TopDomain,
    RiskScore,
    FirstSeen,
    LastSeen,
    NetworkIP,
    AlertUID,
    UID,
    Softwares,
    WebAppFirewall,
    ClickJackingDefence,
    ContentSecurityPolicy,
    CookieXssProtection,
    DataInjectionDefence,
    DomainStatus,
    MissingEPPCodes,
    SecureCookie,
    SetCookieHttpsOnly,
    XFrameOptions,
    X_XssProtection,
    ProviderName,
    ProductName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

DNS events related to ToR proxies (ASIM DNS Schema)

#
Severity
low
Time window
1d
Author
Yaron Fruchtmann
Source
github.com/Azure/Azure-Sentinel

Identifies IP addresses performing DNS lookups associated with common ToR proxies. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM DNS schema

MITRE ATT&CK Coverage

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 3fe3c520-04f1-44b8-8398-782ed21435f8
name: DNS events related to ToR proxies  (ASIM DNS Schema)
description: |
  'Identifies IP addresses performing DNS lookups associated with common ToR proxies.
  This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM DNS schema'
severity: Low
requiredDataConnectors:
  - connectorId: DNS
    dataTypes:
      - DnsEvents
  - connectorId: AzureFirewall
    dataTypes: 
      - AzureDiagnostics
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: InfobloxNIOS
    dataTypes: 
      - Syslog
  - connectorId: GCPDNSDataConnector
    dataTypes: 
      - GCP_DNS_CL
  - connectorId: NXLogDnsLogs
    dataTypes: 
      - NXLog_DNS_Server_CL
  - connectorId: CiscoUmbrellaDataConnector
    dataTypes: 
      - Cisco_Umbrella_dns_CL
  - connectorId: Corelight
    dataTypes: 
      - Corelight_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
tags:
  - ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/DnsEvents/DNS_TorProxies.yaml
    version: 1.0.0
  - Schema: ASIMDns
    SchemaVersion: 0.1.1
query: |
  let torProxies=dynamic(["tor2web.org", "tor2web.com", "torlink.co", "onion.to", "onion.ink", "onion.cab", "onion.nu", "onion.link", 
  "onion.it", "onion.city", "onion.direct", "onion.top", "onion.casa", "onion.plus", "onion.rip", "onion.dog", "tor2web.fi", 
  "tor2web.blutmagie.de", "onion.sh", "onion.lu", "onion.pet", "t2w.pw", "tor2web.ae.org", "tor2web.io", "tor2web.xyz", "onion.lt", 
  "s1.tor-gateways.de", "s2.tor-gateways.de", "s3.tor-gateways.de", "s4.tor-gateways.de", "s5.tor-gateways.de", "hiddenservice.net"]);
  _Im_Dns(domain_has_any=torProxies)
  | extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
  | project-away DomainIndex
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Dvc
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
version: 1.3.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Yaron Fruchtmann
    support:
        tier: Community
    categories:
        domains: [ "Security - Network" ]

Stages and Predicates

Let Binding: torProxies used in Stage 1

let torProxies = dynamic(["tor2web.org", "tor2web.com", "torlink.co", "onion.to", "onion.ink", "onion.cab", "onion.nu", "onion.link", 
"onion.it", "onion.city", "onion.direct", "onion.top", "onion.casa", "onion.plus", "onion.rip", "onion.dog", "tor2web.fi", 
"tor2web.blutmagie.de", "onion.sh", "onion.lu", "onion.pet", "t2w.pw", "tor2web.ae.org", "tor2web.io", "tor2web.xyz", "onion.lt", 
"s1.tor-gateways.de", "s2.tor-gateways.de", "s3.tor-gateways.de", "s4.tor-gateways.de", "s5.tor-gateways.de", "hiddenservice.net"]);

Stage 1: source

_Im_Dns(domain_has_any=torProxies)

Stage 2: extend

| extend HostName = tostring(split(Dvc, ".")[0]), DomainIndex = toint(indexof(Dvc, '.'))

Stage 3: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Dvc, DomainIndex + 1), Dvc)
HostNameDomain =
ifDomainIndex != -1substring(Dvc, (DomainIndex + 1))
elseDvc

Stage 4: project-away

| project-away DomainIndex

Output Fields

These fields are emitted when the rule matches.

FieldSource
HostNameextend
HostNameDomainextend

Dynatrace Application Security - Non-critical runtime vulnerability detection

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

Detect runtime vulnerabilities in your environment insights by snyk

MITRE ATT&CK Coverage

Rule Body

id: ff0af873-a2f2-4233-8412-0ef4e00b0156
name: Dynatrace Application Security - Non-critical runtime vulnerability detection
description: 'Detect runtime vulnerabilities in your environment insights by snyk'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: DynatraceRuntimeVulnerabilities
    dataTypes:
      - DynatraceSecurityProblems
  - connectorId: DTRunVulnCCPDefinition
    dataTypes:
      - DynatraceSecurityProblemsV2_CL
  - connectorId: DTRunVulnV3CCPDefinition
    dataTypes:
      - DynatraceSecurityProblemsV3_CL
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Execution
  - Impact
  - InitialAccess
  - LateralMovement
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1140
  - T1059
  - T1565
  - T1659
  - T1210
  - T1554
  - T1548
eventGroupingSettings:
  aggregationKind: AlertPerResult
query: |
  DynatraceSecurityProblems
  | where DAVISRiskLevel != "CRITICAL"  and Muted == false
  | summarize  arg_max(LastUpdatedTimeStamp, *) by SecurityProblemId
alertDetailsOverride:
  alertDisplayNameFormat: 'Dynatrace Non-critical runtime vulnerability detected - {{DisplayId}} : {{Title}}'
  alertDescriptionFormat: |
    Non-critical runtime vulnerability ({{ExternalVulnerabilityId}}) detected in package ({{PackageName}}), more details available from Dynatrace at {{Url}}.
  alertSeverityColumnName: Severity
customDetails:
  SecProbIdentifier: SecurityProblemId
  DisplayIdentifier: DisplayId
  SecurityProblemUrl: Url
  DAVISRiskLevel: DAVISRiskLevel
  PackageName: PackageName
  Technology: Technology
  VulnerabilityType: VulnerabilityType
  CVEIds: CVEIds
  ExternVulnIdentifier: ExternalVulnerabilityId
  DAVISRiskScore: DAVISRiskScore
  DAVISRiskVector: DAVISRiskVector
  DAVISExposure: DAVISExposure
  DAVISPublicExploit: DAVISPublicExploit
  DAVISDataAssets: DAVISDataAssets
  DAVISVulnFuncUsage: DAVISVulnerableFunctionUsage
incidentConfiguration:
  createIncident: false
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

DynatraceSecurityProblems

Stage 2: where

| where DAVISRiskLevel != "CRITICAL"  and Muted == false

Stage 3: summarize

| summarize  arg_max(LastUpdatedTimeStamp, *) by SecurityProblemId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SecurityProblemIdsummarize

HoneyLabs TI Map IP Entity to CommonSecurityLog

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

'Identifies source-IP matches in CommonSecurityLog (firewall, proxy and other CEF sources) against IP indicators from HoneyLabs honeypot telemetry. A match means a source IP that ran exploit or loader commands against HoneyLabs sensors also appeared as a source in your own logs. Only the structured SourceIP field is matched; destination IPs and IPs merely mentioned inside the free-text Message are ignored, because those are frequently unrelated and generate false positives. Indicator Confidence reflects how much evidence HoneyLabs holds (90 = 100+ observed attacks, 60 = a single sighting); raise the threshold in the query to trade coverage for precision.'

MITRE ATT&CK Coverage

Rule Body

id: c7061f05-d0ed-40e1-862e-bc52e454e3a1
name: HoneyLabs TI Map IP Entity to CommonSecurityLog
version: 1.0.0
kind: Scheduled
description: |
  'Identifies source-IP matches in CommonSecurityLog (firewall, proxy and other CEF sources) against IP indicators from HoneyLabs honeypot telemetry. A match means a source IP that ran exploit or loader commands against HoneyLabs sensors also appeared as a source in your own logs. Only the structured SourceIP field is matched; destination IPs and IPs merely mentioned inside the free-text Message are ignored, because those are frequently unrelated and generate false positives. Indicator Confidence reflects how much evidence HoneyLabs holds (90 = 100+ observed attacks, 60 = a single sighting); raise the threshold in the query to trade coverage for precision.'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: CEF
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let HoneyLabs_IPs = ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | where IsActive == true and ValidUntil > now()
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id
    | where SourceSystem startswith 'HoneyLabs'
    | where ObservableKey == 'ipv4-addr:value'
    | extend TI_ipEntity = tostring(ObservableValue),
             HoneyLabsConfidence = Confidence,
             HoneyLabsLabels = tostring(Data.labels),
             IndicatorDescription = tostring(Data.description),
             HoneyLabsReport = strcat('https://honeylabs.net/lookup/', tostring(ObservableValue));
  HoneyLabs_IPs
  | join kind=innerunique (
      CommonSecurityLog
      | where TimeGenerated >= ago(dt_lookBack)
      | where isnotempty(SourceIP)
      | extend CS_ipEntity = SourceIP
      | extend CommonSecurityLog_TimeGenerated = TimeGenerated
  ) on $left.TI_ipEntity == $right.CS_ipEntity
  | where CommonSecurityLog_TimeGenerated < ValidUntil
  | summarize CommonSecurityLog_TimeGenerated = arg_max(CommonSecurityLog_TimeGenerated, *) by Id, CS_ipEntity
  | project CommonSecurityLog_TimeGenerated, IndicatorDescription, Id, ValidUntil,
    HoneyLabsConfidence, HoneyLabsLabels, HoneyLabsReport, TI_ipEntity, CS_ipEntity, LogSeverity,
    DeviceAction, SourceIP, DestinationIP, DeviceName, SourceUserName
  | extend timestamp = CommonSecurityLog_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CS_ipEntity

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;

let HoneyLabs_IPs is inlined into the numbered stages below.

Stages 1 to 7 define let HoneyLabs_IPs as the rule's main pipeline source. Stages 8 to 12 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: where

| where IsActive == true and ValidUntil > now()

Stage 4: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id

Stage 5: where

| where SourceSystem startswith 'HoneyLabs'

Stage 6: where

| where ObservableKey == 'ipv4-addr:value'

Stage 7: extend

| extend TI_ipEntity = tostring(ObservableValue),
           HoneyLabsConfidence = Confidence,
           HoneyLabsLabels = tostring(Data.labels),
           IndicatorDescription = tostring(Data.description),
           HoneyLabsReport = strcat('https://honeylabs.net/lookup/', tostring(ObservableValue))

Stage 8: join

HoneyLabs_IPs
| join kind=innerunique (
    CommonSecurityLog
    | where TimeGenerated >= ago(dt_lookBack)
    | where isnotempty(SourceIP)
    | extend CS_ipEntity = SourceIP
    | extend CommonSecurityLog_TimeGenerated = TimeGenerated
) on $left.TI_ipEntity == $right.CS_ipEntity

Stage 9: where

where CommonSecurityLog_TimeGenerated < ValidUntil

Stage 10: summarize

summarize CommonSecurityLog_TimeGenerated by Id, CS_ipEntity

Stage 11: project

project CS_ipEntity, CommonSecurityLog_TimeGenerated, DestinationIP, DeviceAction, DeviceName, HoneyLabsConfidence, HoneyLabsLabels, HoneyLabsReport, Id, IndicatorDescription, LogSeverity, SourceIP, SourceUserName, TI_ipEntity, ValidUntil

Stage 12: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CS_ipEntityproject
CommonSecurityLog_TimeGeneratedproject
DestinationIPproject
DeviceActionproject
DeviceNameproject
HoneyLabsConfidenceproject
HoneyLabsLabelsproject
HoneyLabsReportproject
Idproject
IndicatorDescriptionproject
LogSeverityproject
SourceIPproject
SourceUserNameproject
TI_ipEntityproject
ValidUntilproject
timestampextend

Hunt for devices organized by subnet

#
Group by
DeviceId, Joiner, NetworkAddress, ReportId
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

This rule helps you organize devices by subnet in your networks. By doing this, you can identify how many not-onboarded devices, devices not supporting MDE containment, and types of devices live in your subnet ranges.

References

Telemetry Coverage

Rule Body

let isolationSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2", "Linux", "macOS"]);
let containmentSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2"]);
let base = DeviceNetworkInfo
    // Expand all IPs
    | mv-expand todynamic(IPAddresses)
    // Ignore IPv6 addresses
    | where tostring(IPAddresses.IPAddress) !contains ":"
    // Save the Prefix as an extra property and set it to /32 when empty
    | extend Prefix = iff(isnotempty(tostring(IPAddresses.SubnetPrefix)), tostring(IPAddresses.SubnetPrefix), "32");
let networks = base
    // Get network addresses with a non /32 prefix
    | where Prefix != "32"
    // Get the network address related to the IP
    | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
    // Build the IP and Network Address with the CIDR notation
    | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix)
    | extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
    // Join the Device Info information
    | join kind=inner DeviceInfo on DeviceId, ReportId
    // Ignore APIPA addresses
    | where NetworkAddress != "169.254.0.0/16"
    // Ignore merged device IDs
    | where MergedToDeviceId == ""
    // Make a set of all the Device Objects belonging to the same subnet
    | extend DeviceObj = pack(
        "DeviceName", DeviceName,
        "IPAddress", IPAddress,
        "DeviceType", DeviceType,
        "DeviceCategory", DeviceCategory,
        "IsInternetFacing", IsInternetFacing,
        "OnboardingStatus", OnboardingStatus,
        "OSDistribution", OSDistribution,
        "OSPlatform", OSPlatform
    )
    // Make a list of the objects in the same subnet
    | summarize make_set(DeviceObj) by NetworkAddress;
let device_with_host_prefix = base
    // Get network addresses with /32 Prefix to try and match other networks
    | where Prefix == "32"
    // Build the IP Address with the CIDR notation
    | extend IPAddress = strcat(tostring(IPAddresses.IPAddress), "/", Prefix)
    // Join the Device Info information
    | join kind=inner DeviceInfo on DeviceId, ReportId
    // Ignore merged device IDs
    | where MergedToDeviceId == ""
    // Make a set of all the Device Objects
    | extend DeviceObj = pack(
        "DeviceName", DeviceName,
        "IPAddress", IPAddress,
        "DeviceType", DeviceType,
        "DeviceCategory", DeviceCategory,
        "IsInternetFacing", IsInternetFacing,
        "OnboardingStatus", OnboardingStatus,
        "OSDistribution", OSDistribution,
        "OSPlatform", OSPlatform
    )
    | extend Joiner = 1;
let network_addresses = base
    // Get network addresses with a non /32 prefix
    | where Prefix != "32"
    // Get the network address related to the IP
    | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
    | extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
    // Create joiner to find host addresses related to certain networks
    | distinct NetworkAddress
    | extend Joiner = 1;
let networks2 = device_with_host_prefix
    // Try to join /32 IPs
    | join kind=inner network_addresses on Joiner
    // Check if IP is in the network range, and only return those IPs
    | extend InRange = ipv4_is_in_range(IPAddress, NetworkAddress)
    | where InRange == 1
    // Make a list of the objects in the same subnet
    | summarize make_set(DeviceObj) by NetworkAddress;
union networks, networks2
    // Expand the Device Objects
    | mv-expand set_DeviceObj
    // Save the DeviceType, DeviceCategory, and Onboarding Status
    | extend DeviceType = set_DeviceObj.DeviceType
    | extend DeviceCategory = set_DeviceObj.DeviceCategory
    | extend OnboardingStatus = set_DeviceObj.OnboardingStatus
    // Count how many servers, workstations, network devices, iot devices, and ot devices exists in a subnet, the onboarding estate, and OS Distribution
    | summarize Servers = countif(set_DeviceObj.DeviceType=="Server"),
        Workstations = countif(set_DeviceObj.DeviceType=="Workstation"),
        NetworkDevices = countif(set_DeviceObj.DeviceCategory=="NetworkDevice"),
        IoTDevices = countif(set_DeviceObj.DeviceCategory=="IoT"),
        OTDevices = countif(set_DeviceObj.DeviceCategory=="OT"),
        Onboarded = countif(set_DeviceObj.OnboardingStatus=="Onboarded"),
        NotOnboarded = countif(set_DeviceObj.OnboardingStatus!="Onboarded"),
        IsolateSupportedOS = countif((set_DeviceObj.OSDistribution has_any (isolationSupportedOS) or set_DeviceObj.OSPlatform == "Linux") and set_DeviceObj.OnboardingStatus == "Onboarded"),
        ContainSupportedOS = countif(set_DeviceObj.OSDistribution has_any (containmentSupportedOS) and set_DeviceObj.OnboardingStatus == "Onboarded") by NetworkAddress
    // Join the network subnets so we have the device objects again
    | join kind=leftouter networks on NetworkAddress
    | join kind=leftouter networks2 on NetworkAddress
    // Extend Array Concat
    | extend set_DeviceObj = array_concat(set_DeviceObj, set_DeviceObj1)
    // Remove duplicate columns
    | project-away NetworkAddress1, NetworkAddress2, set_DeviceObj1
    // Count how many IPs there are in one subnet
    | extend CountIPs = array_length(set_DeviceObj)
    | sort by CountIPs desc

Stages and Predicates

Parameters

let containmentSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2"]);

Let Binding: isolationSupportedOS

let isolationSupportedOS = dynamic(["Windows11", "Windows10", "WindowsServer2025", "WindowsServer2022", "WindowsServer2019", "WindowsServer2016", "WindowsServer2012R2", "Linux", "macOS"]);

Let Binding: network_addresses

let network_addresses = base
    | where Prefix != "32"
    | extend NetworkAddress = format_ipv4(tostring(IPAddresses.IPAddress), tolong(Prefix))
    | extend NetworkAddress = strcat(NetworkAddress, "/", Prefix)
    | distinct NetworkAddress
    | extend Joiner = 1;

Union (2 Sources)

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

Leg 1: networks

Leg 2: networks2

Applied to the Combined Result

| mv-expand set_DeviceObj | extend DeviceType = set_DeviceObj.DeviceType | extend DeviceCategory = set_DeviceObj.DeviceCategory | extend OnboardingStatus = set_DeviceObj.OnboardingStatus | summarize Servers = countif(set_DeviceObj.DeviceType=="Server"),
        Workstations = countif(set_DeviceObj.DeviceType=="Workstation"),
        NetworkDevices = countif(set_DeviceObj.DeviceCategory=="NetworkDevice"),
        IoTDevices = countif(set_DeviceObj.DeviceCategory=="IoT"),
        OTDevices = countif(set_DeviceObj.DeviceCategory=="OT"),
        Onboarded = countif(set_DeviceObj.OnboardingStatus=="Onboarded"),
        NotOnboarded = countif(set_DeviceObj.OnboardingStatus!="Onboarded"),
        IsolateSupportedOS = countif((set_DeviceObj.OSDistribution has_any (isolationSupportedOS) or set_DeviceObj.OSPlatform == "Linux") and set_DeviceObj.OnboardingStatus == "Onboarded"),
        ContainSupportedOS = countif(set_DeviceObj.OSDistribution has_any (containmentSupportedOS) and set_DeviceObj.OnboardingStatus == "Onboarded") by NetworkAddress | join kind=leftouter networks on NetworkAddress | join kind=leftouter networks2 on NetworkAddress | extend set_DeviceObj = array_concat(set_DeviceObj, set_DeviceObj1) | project-away NetworkAddress1, NetworkAddress2, set_DeviceObj1 | extend CountIPs = array_length(set_DeviceObj) | sort by CountIPs desc

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
IPAddresses.IPAddresscontains:excludes:IPAddresses.IPAddress field:"IPAddresses.IPAddress" value:":"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ContainSupportedOSsummarize
IoTDevicessummarize
IsolateSupportedOSsummarize
NetworkAddresssummarize
NetworkDevicessummarize
NotOnboardedsummarize
OTDevicessummarize
Onboardedsummarize
Serverssummarize
Workstationssummarize
set_DeviceObjextend
CountIPsextend

Infoblox - High Threat Level Query Not Blocked Detected

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

'At least 1 high threat level query generated by single host in 1 hour that is not blocked or redirected. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxCDC.'

MITRE ATT&CK Coverage

Rule Body

id: dc7af829-d716-4774-9d6f-03d9aa7c27a4
name: Infoblox - High Threat Level Query Not Blocked Detected
description: |
  'At least 1 high threat level query generated by single host in 1 hour that is not blocked or redirected. Customize query count, scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxCDC**](https://aka.ms/sentinel-InfobloxCloudDataConnector-parser).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1565
query: |
  let threshold = 1;
  InfobloxCDC
  | where DeviceEventClassID has_cs "RPZ"
  | where ThreatLevel_Score >=80
  | where InfobloxB1PolicyAction == "Log" or SimplifiedDeviceAction == "PASSTHRU"
  | summarize count() by SourceIP
  | where count_ > threshold
  | join kind=inner (InfobloxCDC
    | where DeviceEventClassID has_cs "RPZ"
    | where ThreatLevel_Score >=80
    | where InfobloxB1PolicyAction == "Log" or SimplifiedDeviceAction == "PASSTHRU"
    ) on SourceIP
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
      - identifier: OSVersion
        columnName: InfobloxB1SrcOSVersion
      - identifier: FullName
        columnName: SourceUserName
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DestinationDnsDomain
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: ThreatProperty
      - identifier: Category
        columnName: ThreatClass
customDetails:
  SourceMACAddress: SourceMACAddress
  InfobloxB1FeedName: InfobloxB1FeedName
  InfobloxB1Network: InfobloxB1Network
  InfobloxB1Action: InfobloxB1PolicyAction
  InfobloxB1PolicyName: InfobloxB1PolicyName
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 1;

Stage 1: source

InfobloxCDC

Stage 2: where

| where DeviceEventClassID has_cs "RPZ"

Stage 3: where

| where ThreatLevel_Score >=80

Stage 4: where

| where InfobloxB1PolicyAction == "Log" or SimplifiedDeviceAction == "PASSTHRU"

Stage 5: summarize

| summarize count() by SourceIP

Stage 6: where

| where count_ > threshold

Stage 7: join

| join kind=inner (InfobloxCDC
  | where DeviceEventClassID has_cs "RPZ"
  | where ThreatLevel_Score >=80
  | where InfobloxB1PolicyAction == "Log" or SimplifiedDeviceAction == "PASSTHRU"
  ) on SourceIP

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIPsummarize

Infoblox - SOC Insight Detected - API Source

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

'Infoblox SOC Insight detected in logs sourced via REST API. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called InfobloxInsight.'

MITRE ATT&CK Coverage

Rule Body

id: cf9847bb-ab46-4050-bb81-75cab3f893dc
name: Infoblox - SOC Insight Detected - API Source
description: |
  'Infoblox SOC Insight detected in logs sourced via REST API. Customize scheduling, responses and more. This rule depends on a parser based on a Kusto Function to work as expected called [**InfobloxInsight**](https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Infoblox%20SOC%20Insights/Parsers/InfobloxInsight.yaml).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: InfobloxSOCInsightsDataConnector_API
    dataTypes: 
      - InfobloxInsight
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1565
query: |
  InfobloxInsight
  | summarize arg_max(TimeGenerated, *) by InfobloxInsightID
entityMappings:
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: ObjectGuid
        columnName: InfobloxInsightID
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: ThreatClass
      - identifier: Category
        columnName: ThreatProperty
customDetails:
  InfobloxInsightID: InfobloxInsightID
  Severity: Priority
  LastSeen: LastSeen
  FirstSeen: FirstSeen
  FeedSource: FeedSource
  Status: Status
  PersistentDate: PersistentDate
  SpreadingDate: SpreadingDate
  BlockedHits: BlockedCount
  UnblockedHits: NotBlockedCount
  TotalHits: EventsCount
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Infoblox - SOC Insight - {{ThreatClass}} {{ThreatProperty}}'
  alertDescriptionFormat: 'Observed via API. {{ThreatFamily}}. Last Observation: {{LastSeen}}'
  alertSeverityColumnName: IncidentSeverity
incidentConfiguration:
  createIncident: true
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

InfobloxInsight

Stage 2: summarize

| summarize arg_max(TimeGenerated, *) by InfobloxInsightID

Output Fields

These fields are emitted when the rule matches.

FieldSource
InfobloxInsightIDsummarize

Pathlock TDnR - IBAN Change Documents

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

Detects changes to IBAN (International Bank Account Number) records in SAP, forwarded by Pathlock Threat Detection and Response. Unauthorized IBAN modifications are a strong indicator of payment fraud or account takeover targeting financial transactions.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c11
name: Pathlock TDnR - IBAN Change Documents
kind: Scheduled
description: >-
  Detects changes to IBAN (International Bank Account Number) records in SAP, forwarded by Pathlock
  Threat Detection and Response. Unauthorized IBAN modifications are a strong indicator of
  payment fraud or account takeover targeting financial transactions.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1565
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CHANGEDOC_IBAN"
  | 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 == "CHANGEDOC_IBAN"

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

Pathlock TDnR - Vendor Change Documents

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

Detects changes to vendor (creditor) master records in SAP, forwarded by Pathlock Threat Detection and Response. Unauthorized vendor modifications may indicate fraud targeting accounts payable processes, including payment redirection to attacker-controlled accounts.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c13
name: Pathlock TDnR - Vendor Change Documents
kind: Scheduled
description: >-
  Detects changes to vendor (creditor) master records in SAP, forwarded by Pathlock Threat Detection
  and Response. Unauthorized vendor modifications may indicate fraud targeting accounts payable
  processes, including payment redirection to attacker-controlled accounts.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1565
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CHANGEDOC_KRED"
  | 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 == "CHANGEDOC_KRED"

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

Radiflow - Exploit Detected

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

'Generates an incident when the use of an exploit is detected by Radiflow's iSID.'

MITRE ATT&CK Coverage

Rule Body

id: 6c028ebd-03ca-41cb-bce7-5727ddb43731
name: Radiflow - Exploit Detected
description: |
  'Generates an incident when the use of an exploit is detected by Radiflow's iSID.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: RadiflowIsid
    dataTypes:
      - RadiflowEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- PrivilegeEscalation
- LateralMovement
relevantTechniques:
- T0819
- T0866
- T0890
query: |
  RadiflowEvent
  | where DeviceProduct =~ 'iSID'
  | where
      (
          EventClassID in (34, 53, 67, 68, 69, 70, 71, 179)
          or EventMessage has 'Exploit'
      )
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: 'Exploit Detected: {{EventMessage}}'
  alertDescriptionFormat: "A possible exploit attempt has been detected. Check the details of this incident for further information.\n\nMessage: {{EventMessage}}\nSource device: {{SourceIP}} \nDestination device (if any): {{DestinationIP}} "
  alertSeverityColumnName: EventSeverity
  alertDynamicProperties: []
suppressionEnabled: false
suppressionDuration: 5h
customDetails:
  SourceMAC: SourceMACAddress
  SourceType: SourceType
  SourceVendor: SourceVendor
  SourceVLAN: SourceVLAN
  DestinationMAC: DestinationMACAddress
  DestinationType: DestinationType
  DestinationVendor: DestinationVendor
  Port: Port
  Protocol: Protocol
  SourceHostName: SourceHostName
  SourceIP: SourceIP
  DestinationHostName: DestinationHostName
  DestinationIP: DestinationIP
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: SourceHostName
  - identifier: NetBiosName
    columnName: SourceHostName
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: DestinationHostName
  - identifier: NetBiosName
    columnName: DestinationHostName
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SourceIP
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: DestinationIP
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

RadiflowEvent

Stage 2: where

| where DeviceProduct =~ 'iSID'

Stage 3: where

| where
    (
        EventClassID in (34, 53, 67, 68, 69, 70, 71, 179)
        or EventMessage has 'Exploit'
    )

Indicators

These rows show field, operator, and value matches.

SAP BTP - Cloud Integration tampering with security material

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

Identifies operations on security material (credentials, certificates, and keys) within SAP Cloud Integration. This includes credentials (passwords/secrets), X.509 certificates and key pairs, and PGP keys. Unauthorized manipulation of security material could indicate an attacker attempting to: - Gain access to external systems using stored credentials - Intercept or tamper with encrypted communications - Establish persistence through certificate manipulation - Cover tracks by deleting security artifacts

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Credential Access

Rule Body

id: 8d5f3a1b-9c2e-4f7d-b8a6-1e4c7f9d2b5a
kind: Scheduled
name: SAP BTP - Cloud Integration tampering with security material
description: |
  Identifies operations on security material (credentials, certificates, and keys) within SAP Cloud Integration.
  This includes credentials (passwords/secrets), X.509 certificates and key pairs, and PGP keys.
  Unauthorized manipulation of security material could indicate an attacker attempting to:
  - Gain access to external systems using stored credentials
  - Intercept or tamper with encrypted communications
  - Establish persistence through certificate manipulation
  - Cover tracks by deleting security artifacts
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SAPBTPAuditEvents
    dataTypes:
      - SAPBTPAuditLog_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1552
  - T1070
query: |
  let securityMaterialTypes = dynamic(["Credential", "X.509 Certificate", "X.509 Key-Pair", "PGP Public Keys", "PGP Secret Keys"]);
  let keystoreActions = dynamic(["Create", "Update", "Change", "Delete"]);
  SAPBTPAuditLog_CL
  | where Category == "audit.security-events"
  | extend data_s = tostring(Message.data),
           ipAddress = tostring(Message.ip)
  | extend parsedData = parse_json(data_s)
  | extend action = tostring(parsedData.action),
           objectType = tostring(parsedData.objectType),
           objectId = tostring(parsedData.objectId),
           keystoreName = tostring(parsedData.attributes["Keystore Name"])
  | where objectType in (securityMaterialTypes)
  | where 
      (objectType == "Credential" and action in ("PasswordStore", "PasswordUpdate", "PasswordDelete"))
      or 
      (objectType != "Credential" and action in (keystoreActions))
  | extend normalizedAction = case(
      action == "PasswordStore", "created",
      action == "PasswordUpdate", "updated",
      action == "PasswordDelete", "deleted",
      action == "Create", "created",
      action == "Update", "updated",
      action == "Change", "changed",
      action == "Delete", "deleted",
      action
  )
  | extend MessageText = case(
      objectType == "Credential", strcat("Security credential '", objectId, "' was ", normalizedAction),
      isnotempty(keystoreName), strcat(objectType, " '", objectId, "' was ", normalizedAction, " in keystore '", keystoreName, "'"),
      strcat(objectType, " '", objectId, "' was ", normalizedAction)
  )
  | project
      UpdatedOn,
      UserName,
      MessageText,
      ObjectType = objectType,
      ObjectId = objectId,
      Action = action,
      NormalizedAction = normalizedAction,
      KeystoreName = keystoreName,
      Tenant,
      ipAddress,
      CloudApp = "SAP Cloud Integration"
  | extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ipAddress
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: CloudApp
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP Cloud Integration: {{MessageText}}'
  alertDescriptionFormat: |
    {{MessageText}} by {{UserName}} from IP {{ipAddress}}.
        
    This could indicate:
    - Legitimate security material management
    - Unauthorized credential or certificate manipulation
    - Attacker tampering with security artifacts to gain access or cover tracks
customDetails:
  ObjectType: ObjectType
  ObjectId: ObjectId
  Action: Action
  KeystoreName: KeystoreName
  SourceIP: ipAddress
version: 1.0.1

Stages and Predicates

Parameters

let securityMaterialTypes = dynamic(["Credential", "X.509 Certificate", "X.509 Key-Pair", "PGP Public Keys", "PGP Secret Keys"]);
let keystoreActions = dynamic(["Create", "Update", "Change", "Delete"]);

Stage 1: source

SAPBTPAuditLog_CL

Stage 2: where

| where Category == "audit.security-events"

Stage 3: extend (3 consecutive steps)

| extend data_s = tostring(Message.data),
         ipAddress = tostring(Message.ip)
| extend parsedData = parse_json(data_s)
| extend action = tostring(parsedData.action),
         objectType = tostring(parsedData.objectType),
         objectId = tostring(parsedData.objectId),
         keystoreName = tostring(parsedData.attributes["Keystore Name"])

Stage 4: where

| where objectType in (securityMaterialTypes)

Stage 5: where

| where 
    (objectType == "Credential" and action in ("PasswordStore", "PasswordUpdate", "PasswordDelete"))
    or 
    (objectType != "Credential" and action in (keystoreActions))

Stage 6: extend

| extend normalizedAction = case(
    action == "PasswordStore", "created",
    action == "PasswordUpdate", "updated",
    action == "PasswordDelete", "deleted",
    action == "Create", "created",
    action == "Update", "updated",
    action == "Change", "changed",
    action == "Delete", "deleted",
    action
)
normalizedAction =
ifaction == "PasswordStore""created"
elifaction == "PasswordUpdate""updated"
elifaction == "PasswordDelete""deleted"
elifaction == "Create""created"
elifaction == "Update""updated"
elifaction == "Change""changed"
elifaction == "Delete""deleted"
elseaction

Stage 7: extend

| extend MessageText = case(
    objectType == "Credential", strcat("Security credential '", objectId, "' was ", normalizedAction),
    isnotempty(keystoreName), strcat(objectType, " '", objectId, "' was ", normalizedAction, " in keystore '", keystoreName, "'"),
    strcat(objectType, " '", objectId, "' was ", normalizedAction)
)
MessageText =
ifobjectType == "Credential"strcat("Security credential '", objectId, "' was ", normalizedAction)
elifisnotempty(keystoreName)strcat(objectType, " '", objectId, "' was ", normalizedAction, " in keystore '", keystoreName, "'")
elsestrcat(objectType, " '", objectId, "' was ", normalizedAction)

Stage 8: project

| project
    UpdatedOn,
    UserName,
    MessageText,
    ObjectType = objectType,
    ObjectId = objectId,
    Action = action,
    NormalizedAction = normalizedAction,
    KeystoreName = keystoreName,
    Tenant,
    ipAddress,
    CloudApp = "SAP Cloud Integration"

Stage 9: extend

| extend AccountName = split(UserName, "@")[0], UPNSuffix = split(UserName, "@")[1]

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • audit.security-events
field:"Category" kind:eq value:"audit.security-events"
actionin
  • Change
  • Create
  • Delete
  • PasswordDelete
  • PasswordStore
  • PasswordUpdate
  • Update
field:"action" kind:in
objectTypeeq
  • Credential
field:"objectType" kind:eq value:"Credential"
objectTypein
  • Credential
  • PGP Public Keys
  • PGP Secret Keys
  • X.509 Certificate
  • X.509 Key-Pair
field:"objectType" kind:in
objectTypene
  • Credential
field:"objectType" kind:ne value:"Credential"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
CloudAppproject
KeystoreNameproject
MessageTextproject
NormalizedActionproject
ObjectIdproject
ObjectTypeproject
Tenantproject
UpdatedOnproject
UserNameproject
ipAddressproject
AccountNameextend
UPNSuffixextend

SUNBURST network beacons

#

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: ce1e7025-866c-41f3-9b08-ec170e05e73e
name: SUNBURST network beacons
description: |
  Identifies SolarWinds SUNBURST domain beacon IOCs in DeviceNetworkEvents
  References:
  - https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html
  - https://gist.github.com/olafhartong/71ffdd4cab4b6acd5cbcd1a0691ff82f
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1195
  - T1059
  - T1546
query:  |
  let SunburstURL=dynamic(["panhardware.com","databasegalore.com","avsvmcloud.com","freescanonline.com","thedoccloud.com","deftsecurity.com"]);
  DeviceNetworkEvents
  | where ActionType == "ConnectionSuccess"
  | where RemoteUrl in(SunburstURL)
  | extend HashAlgorithm = 'MD5'
  | extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName 
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName 
        columnName: InitiatingProcessAccountUpn
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: UPNSuffix
        columnName: InitiatingProcessAccountDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: RemoteIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RemoteUrl
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: HashAlgorithm
      - identifier: Value
        columnName: InitiatingProcessMD5
version: 1.0.5
kind: Scheduled

Stages and Predicates

Parameters

let SunburstURL = dynamic(["panhardware.com","databasegalore.com","avsvmcloud.com","freescanonline.com","thedoccloud.com","deftsecurity.com"]);

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where ActionType == "ConnectionSuccess"

Stage 3: where

| where RemoteUrl in(SunburstURL)

Stage 4: extend (3 consecutive steps)

| extend HashAlgorithm = 'MD5'
| extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • ConnectionSuccess corpus 4 (kusto 4)
field:"ActionType" kind:eq value:"ConnectionSuccess"
RemoteUrlin
  • avsvmcloud.com
  • databasegalore.com
  • deftsecurity.com
  • freescanonline.com
  • panhardware.com
  • thedoccloud.com
field:"DestinationHostname" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
HashAlgorithmextend
DomainIndexextend
HostNameextend
HostNameDomainextend

TI Map Email entity to SigninLogs

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

Identifies a match in SigninLogs table from any Email IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 4b5a7f32-899d-4d22-8de2-0ec90b911a72
name: TI Map Email entity to SigninLogs
description: |
  'Identifies a match in SigninLogs table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  let Signins = materialize(union isfuzzy=true
  ( SigninLogs | where TimeGenerated >= ago(dt_lookBack)),
  ( AADNonInteractiveUserSignInLogs | where TimeGenerated >= ago(dt_lookBack)
      | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails))
  | where isnotempty(UserPrincipalName) and UserPrincipalName matches regex emailregex
  | extend UserPrincipalName = tolower(UserPrincipalName)
  | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
  | extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
  | extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
  | extend SigninLogs_TimeGenerated = TimeGenerated);
  let SigninUPNs = Signins | distinct UserPrincipalName | summarize make_list(UserPrincipalName);
  ThreatIntelIndicators
  //Filtering the table for Email related IOCs
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "email-addr"
  | extend EmailSenderAddress = ObservableValue
  | extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | extend Id = tostring(split(Id, "--")[2])
  | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
  | where TimeGenerated >= ago(ioc_lookBack)
  | where EmailSenderAddress in (SigninUPNs)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
    | project-reorder *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type
  | join kind=innerunique (Signins) on $left.EmailSenderAddress == $right.UserPrincipalName
  | where SigninLogs_TimeGenerated < ValidUntil
  | summarize SigninLogs_TimeGenerated = arg_max(SigninLogs_TimeGenerated, *) by Id, UserPrincipalName
  | extend Description = tostring(parse_json(Data).description)
  | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
  | project SigninLogs_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, EmailSenderAddress, IPAddress, UserPrincipalName, AppDisplayName, StatusCode, StatusDetails, NetworkLocationDetails, Type, Url
  | extend Name = tostring(split(UserPrincipalName, '@', 0)[0]), UPNSuffix = tostring(split(UserPrincipalName, '@', 1)[0])
  | extend timestamp = SigninLogs_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix 
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.12
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Let Binding: Signins used in Stage 12

let Signins = materialize(union isfuzzy=true
( SigninLogs | where TimeGenerated >= ago(dt_lookBack)),
( AADNonInteractiveUserSignInLogs | where TimeGenerated >= ago(dt_lookBack)
    | extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails))
| where isnotempty(UserPrincipalName) and UserPrincipalName matches regex emailregex
| extend UserPrincipalName = tolower(UserPrincipalName)
| extend Status = todynamic(Status), LocationDetails = todynamic(LocationDetails)
| extend StatusCode = tostring(Status.errorCode), StatusDetails = tostring(Status.additionalDetails)
| extend State = tostring(LocationDetails.state), City = tostring(LocationDetails.city), Region = tostring(LocationDetails.countryOrRegion)
| extend SigninLogs_TimeGenerated = TimeGenerated);

Let Binding: SigninUPNs used in Stage 6

let SigninUPNs = Signins | distinct UserPrincipalName | summarize make_list(UserPrincipalName);

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

| extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))

Stage 3: where

| where IndicatorType == "email-addr"

Stage 4: extend (5 consecutive steps)

| extend EmailSenderAddress = ObservableValue
| extend EmailSourceDomain = substring(EmailSenderAddress, indexof(EmailSenderAddress, "@") + 1, strlen(EmailSenderAddress) - indexof(EmailSenderAddress, "@") - 1)
| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
| extend Id = tostring(split(Id, "--")[2])
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")

Stage 5: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 6: where

| where EmailSenderAddress in (SigninUPNs)

Stage 7: summarize

| summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue

Stage 8: where

| where IsActive and (ValidUntil > now() or isempty(ValidUntil))

Stage 9: extend

| extend Description = tostring(parse_json(Data).description)

Stage 10: where

| where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"

Stage 11: project-reorder

| project-reorder *, TrafficLightProtocolLevel, EmailSenderAddress, EmailSourceDomain, Type

Stage 12: join

join kind=innerunique (Signins) on $left.EmailSenderAddress, $right.UserPrincipalName

Stage 13: where

where SigninLogs_TimeGenerated < ValidUntil

Stage 14: summarize

summarize SigninLogs_TimeGenerated by Id, UserPrincipalName

Stage 15: extend

extend Description

Stage 16: extend

extend ActivityGroupNames

Stage 17: project

project ActivityGroupNames, AppDisplayName, Confidence, Description, EmailSenderAddress, IPAddress, Id, NetworkLocationDetails, SigninLogs_TimeGenerated, StatusCode, StatusDetails, Type, Url, UserPrincipalName, ValidUntil

Stage 18: extend

extend Name, UPNSuffix

Stage 19: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DescriptioncontainsState: falsepos;excludes:Description field:"Description" value:"State: falsepos;"
DescriptioncontainsState: inactive;excludes:Description field:"Description" value:"State: inactive;"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AppDisplayNameproject
Confidenceproject
Descriptionproject
EmailSenderAddressproject
IPAddressproject
Idproject
NetworkLocationDetailsproject
SigninLogs_TimeGeneratedproject
StatusCodeproject
StatusDetailsproject
Typeproject
Urlproject
UserPrincipalNameproject
ValidUntilproject
Nameextend
UPNSuffixextend
timestampextend

TIE DCShadow

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

'Searches for DCShadow attacks.'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 874e3530-552e-437b-ba2e-227979e7e43c
name: TIE DCShadow
description: |
  'Searches for DCShadow attacks.'
severity: High
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1207
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 == "DCShadow"
    | 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 == "DCShadow"

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

Unauthenticated API Endpoint with Sensitive Data

#
Status
available
Severity
high
Time window
375m
Group by
ApplicationId, StraTenantId
Source
github.com/Azure/Azure-Sentinel

Detects internet-facing API endpoints with no authentication that handle sensitive data classifications (PII, Financial, PHI, Confidential). Uses StratoSecure_ApiInventory_CL from Phase 6 API inventory discovery.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 479bc6ef-ecb3-496f-9a2a-38c9635d77d3
name: Unauthenticated API Endpoint with Sensitive Data
description: |
  Detects internet-facing API endpoints with no authentication that handle
  sensitive data classifications (PII, Financial, PHI, Confidential).
  Uses StratoSecure_ApiInventory_CL from Phase 6 API inventory discovery.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: StratoSecurePush
    dataTypes:
      - StratoSecure_ApiInventory_CL
queryFrequency: 6h
queryPeriod: 375m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let sensitiveDataClasses = dynamic(["PII", "Financial", "PHI", "Confidential"]);
  StratoSecure_ApiInventory_CL
      | where TimeGenerated >= ago(6h)
      | where InternetExposure == true
      | where AuthType =~ "none" or isempty(AuthType)
      | where DataClassification has_any (sensitiveDataClasses)
      | summarize EndpointCount = count(), Endpoints = make_set(Path, 50),
          Methods = make_set(Method), DataClasses = make_set(DataClassification),
          EndTime = max(TimeGenerated)
          by ApplicationId = ServiceName, StraTenantId
      | project TimeGenerated = EndTime, ApplicationId, StraTenantId,
          EndpointCount, Endpoints, Methods, DataClasses
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: ApplicationId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let sensitiveDataClasses = dynamic(["PII", "Financial", "PHI", "Confidential"]);

Stage 1: source

StratoSecure_ApiInventory_CL

Stage 2: where

| where TimeGenerated >= ago(6h)

Stage 3: where

| where InternetExposure == true

Stage 4: where

| where AuthType =~ "none" or isempty(AuthType)

Stage 5: where

| where DataClassification has_any (sensitiveDataClasses)

Stage 6: summarize

| summarize EndpointCount = count(), Endpoints = make_set(Path, 50),
        Methods = make_set(Method), DataClasses = make_set(DataClassification),
        EndTime = max(TimeGenerated)
        by ApplicationId = ServiceName, StraTenantId

Stage 7: project

| project TimeGenerated = EndTime, ApplicationId, StraTenantId,
        EndpointCount, Endpoints, Methods, DataClasses

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AuthTypeeq
  • none
field:"AuthType" kind:eq value:"none"
AuthTypeis_null
  • (no value, null check)
field:"AuthType" kind:is_null
DataClassificationmatch
  • Confidential transforms: term
  • Financial transforms: term
  • PHI transforms: term
  • PII transforms: term
field:"DataClassification" kind:match
InternetExposureeq
  • true
field:"InternetExposure" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ApplicationIdproject
DataClassesproject
EndpointCountproject
Endpointsproject
Methodsproject
StraTenantIdproject
TimeGeneratedproject

VMware ESXi - SSH Enable on ESXi Host

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

'Detects when vim-cmd is used to enable SSH on an ESXi host'

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Rule Body

id: 4f5b5d79-ff4e-4edd-ae3e-9e1187f0b924
name: VMware ESXi - SSH Enable on ESXi Host
description: |
  'Detects when vim-cmd is used to enable SSH on an ESXi host'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1021
query: |
  VMwareESXi
  | where SyslogMessage has_all ("vim-cmd","hostsvc/enable_ssh")
  | extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)
  | extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DstHostname
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: SrcUsername
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

VMwareESXi

Stage 2: where

| where SyslogMessage has_all ("vim-cmd","hostsvc/enable_ssh")

Stage 3: extend

| extend SrcUsername = extract(@'\[info\]\s+\[(.*?)\]', 1, SyslogMessage)

Stage 4: extend

| extend DstHostname = extract(@'\[\d+\]\s+\[(.*?)\s+on', 1, SyslogMessage)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SyslogMessagematch
  • hostsvc/enable_ssh transforms: term
  • vim-cmd transforms: term
field:"SyslogMessage" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcUsernameextend
DstHostnameextend

Back to all Kusto rules