Detection rules › Kusto

Kusto rules: 0h

Account added and removed from privileged groups

#
Severity
low
Time window
1d
Group by
$left.AccountAdded_GroupAddedTo_AddingAccount, $right.AccountRemoved_GroupRemovedFrom_RemovingAccount
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies accounts that are added to a privileged group and then quickly removed, which could be a sign of compromise.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 7efc75ce-e2a4-400f-a8b1-283d3b0f2c60
name: Account added and removed from privileged groups
description: |
  'Identifies accounts that are added to a privileged group and then quickly removed, which could be a sign of compromise.'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098
  - T1078
query: |
  let WellKnownLocalSID = "S-1-5-32-5[0-9][0-9]$";
  let WellKnownGroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-498$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1000$";
  let AC_Add =
  (union isfuzzy=true
  (SecurityEvent
  // Event ID related to member addition.
  | where EventID in (4728, 4732,4756)
  | where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
  | parse EventData with * '"MemberName">' * '=' AccountAdded ",OU" *
  | where isnotempty(AccountAdded)
  | extend GroupAddedTo = TargetUserName, AddingAccount = Account
  | extend  AccountAdded_GroupAddedTo_AddingAccount = strcat(AccountAdded, "||", GroupAddedTo, "||", AddingAccount )
  | project AccountAdded_GroupAddedTo_AddingAccount, AccountAddedTime = TimeGenerated
  ),
  (WindowsEvent
  // Event ID related to member addition.
  | where EventID in (4728, 4732,4756)
  | extend TargetSid = tostring(EventData.TargetSid)
  | where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
  | parse EventData.MemberName with * '"MemberName">' * '=' AccountAdded ",OU" *
  | where isnotempty(AccountAdded)
  | extend TargetUserName = tostring(EventData.TargetUserName)
  | extend AddingAccount =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend GroupAddedTo = TargetUserName
  | extend  AccountAdded_GroupAddedTo_AddingAccount = strcat(AccountAdded, "||", GroupAddedTo, "||", AddingAccount )
  | project AccountAdded_GroupAddedTo_AddingAccount, AccountAddedTime = TimeGenerated
  )
  );
  let AC_Remove =
  ( union isfuzzy=true
  (SecurityEvent
  // Event IDs related to member removal.
  | where EventID in (4729,4733,4757)
  | where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
  | parse EventData with * '"MemberName">' * '=' AccountRemoved ",OU" *
  | where isnotempty(AccountRemoved)
  | extend GroupRemovedFrom = TargetUserName, RemovingAccount = Account
  | extend AccountRemoved_GroupRemovedFrom_RemovingAccount = strcat(AccountRemoved, "||", GroupRemovedFrom, "||", RemovingAccount)
  | project AccountRemoved_GroupRemovedFrom_RemovingAccount, AccountRemovedTime = TimeGenerated, Computer, AccountRemoved = tolower(AccountRemoved),
  RemovingAccount, RemovingAccountLogonId = SubjectLogonId, GroupRemovedFrom = TargetUserName, TargetDomainName
  ),
  (WindowsEvent
  // Event IDs related to member removal.
  | where EventID in (4729,4733,4757)
  | extend TargetSid = tostring(EventData.TargetSid)
  | where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
  | parse EventData.MemberName with * '"MemberName">' * '=' AccountRemoved ",OU" *
  | where isnotempty(AccountRemoved)
  | extend TargetUserName = tostring(EventData.TargetUserName)
  | extend RemovingAccount =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend GroupRemovedFrom = TargetUserName
  | extend AccountRemoved_GroupRemovedFrom_RemovingAccount = strcat(AccountRemoved, "||", GroupRemovedFrom, "||", RemovingAccount)
  | extend RemovedAccountLogonId= tostring(EventData.SubjectLogonId)
  | extend TargetDomainName = tostring(EventData.TargetDomainName)
  | project AccountRemoved_GroupRemovedFrom_RemovingAccount, AccountRemovedTime = TimeGenerated, Computer, AccountRemoved = tolower(AccountRemoved),
  RemovingAccount, RemovedAccountLogonId, GroupRemovedFrom = TargetUserName, TargetDomainName
  ));
  AC_Add
  | join kind = inner AC_Remove 
  on $left.AccountAdded_GroupAddedTo_AddingAccount == $right.AccountRemoved_GroupRemovedFrom_RemovingAccount
  | extend DurationinSecondAfter_Removed = datetime_diff ('second', AccountRemovedTime, AccountAddedTime)
  | where DurationinSecondAfter_Removed > 0
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend RemovedAccountName = tostring(split(AccountRemoved, @"\")[1]), RemovedAccountNTDomain = tostring(split(AccountRemoved, @"\")[0])
  | extend RemovingAccountName = tostring(split(RemovingAccount, @"\")[1]), RemovingAccountNTDomain = tostring(split(RemovingAccount, @"\")[0])
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountRemoved
      - identifier: Name
        columnName: RemovedAccountName
      - identifier: NTDomain
        columnName: RemovedAccountNTDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: RemovingAccount
      - identifier: Name
        columnName: RemovingAccountName
      - identifier: NTDomain
        columnName: RemovingAccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.1.5
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let WellKnownLocalSID = "S-1-5-32-5[0-9][0-9]$";

Let Binding: WellKnownGroupSID

let WellKnownGroupSID = "S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-498$|S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1000$";

Let Binding: AC_Remove

let AC_Remove = ( union isfuzzy=true
(SecurityEvent
| where EventID in (4729,4733,4757)
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
| parse EventData with * '"MemberName">' * '=' AccountRemoved ",OU" *
| where isnotempty(AccountRemoved)
| extend GroupRemovedFrom = TargetUserName, RemovingAccount = Account
| extend AccountRemoved_GroupRemovedFrom_RemovingAccount = strcat(AccountRemoved, "||", GroupRemovedFrom, "||", RemovingAccount)
| project AccountRemoved_GroupRemovedFrom_RemovingAccount, AccountRemovedTime = TimeGenerated, Computer, AccountRemoved = tolower(AccountRemoved),
RemovingAccount, RemovingAccountLogonId = SubjectLogonId, GroupRemovedFrom = TargetUserName, TargetDomainName
),
(WindowsEvent
| where EventID in (4729,4733,4757)
| extend TargetSid = tostring(EventData.TargetSid)
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
| parse EventData.MemberName with * '"MemberName">' * '=' AccountRemoved ",OU" *
| where isnotempty(AccountRemoved)
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend RemovingAccount =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend GroupRemovedFrom = TargetUserName
| extend AccountRemoved_GroupRemovedFrom_RemovingAccount = strcat(AccountRemoved, "||", GroupRemovedFrom, "||", RemovingAccount)
| extend RemovedAccountLogonId= tostring(EventData.SubjectLogonId)
| extend TargetDomainName = tostring(EventData.TargetDomainName)
| project AccountRemoved_GroupRemovedFrom_RemovingAccount, AccountRemovedTime = TimeGenerated, Computer, AccountRemoved = tolower(AccountRemoved),
RemovingAccount, RemovedAccountLogonId, GroupRemovedFrom = TargetUserName, TargetDomainName
));

Union isfuzzy=true (2 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
| where EventID in (4728, 4732,4756)
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
| parse EventData with * '"MemberName">' * '=' AccountAdded ",OU" *
| where isnotempty(AccountAdded)
| extend GroupAddedTo = TargetUserName, AddingAccount = Account
| extend  AccountAdded_GroupAddedTo_AddingAccount = strcat(AccountAdded, "||", GroupAddedTo, "||", AddingAccount )
| project AccountAdded_GroupAddedTo_AddingAccount, AccountAddedTime = TimeGenerated

Leg 2: WindowsEvent

WindowsEvent
| where EventID in (4728, 4732,4756)
| extend TargetSid = tostring(EventData.TargetSid)
| where TargetSid matches regex WellKnownLocalSID or TargetSid matches regex WellKnownGroupSID
| parse EventData.MemberName with * '"MemberName">' * '=' AccountAdded ",OU" *
| where isnotempty(AccountAdded)
| extend TargetUserName = tostring(EventData.TargetUserName)
| extend AddingAccount =  strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
| extend GroupAddedTo = TargetUserName
| extend  AccountAdded_GroupAddedTo_AddingAccount = strcat(AccountAdded, "||", GroupAddedTo, "||", AddingAccount )
| project AccountAdded_GroupAddedTo_AddingAccount, AccountAddedTime = TimeGenerated

Applied to the Combined Result

| join kind = inner AC_Remove 
on $left.AccountAdded_GroupAddedTo_AddingAccount == $right.AccountRemoved_GroupRemovedFrom_RemovingAccount | extend DurationinSecondAfter_Removed = datetime_diff ('second', AccountRemovedTime, AccountAddedTime) | where DurationinSecondAfter_Removed > 0 | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.')) | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer) | extend RemovedAccountName = tostring(split(AccountRemoved, @"\")[1]), RemovedAccountNTDomain = tostring(split(AccountRemoved, @"\")[0]) | extend RemovingAccountName = tostring(split(RemovingAccount, @"\")[1]), RemovingAccountNTDomain = tostring(split(RemovingAccount, @"\")[0]) | project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AccountAddedis_not_null
  • (no value, null check)
field:"AccountAdded" kind:is_not_null
AccountRemovedis_not_null
  • (no value, null check)
field:"AccountRemoved" kind:is_not_null
DurationinSecondAfter_Removedgt
  • 0 transforms: number
field:"DurationinSecondAfter_Removed" kind:gt value:"0"
EventIDin
  • 4728
  • 4729
  • 4732
  • 4733
  • 4756
  • 4757
field:"EventID" kind:in
TargetSidregex_match
    • S-1-5-21-[0-9]*-[0-9]*-[0-9]*-5[0-9][0-9]$
    • S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1102$
    • S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1103$
    • S-1-5-21-[0-9]*-[0-9]*-[0-9]*-498$
    • S-1-5-21-[0-9]*-[0-9]*-[0-9]*-1000$
  • S-1-5-32-5[0-9][0-9]$
field:"TargetSid" kind:regex_match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountAddedTimeproject
AccountAdded_GroupAddedTo_AddingAccountproject
DurationinSecondAfter_Removedextend
HostNameextend
HostNameDomainextend
RemovedAccountNTDomainextend
RemovedAccountNameextend
RemovingAccountNTDomainextend
RemovingAccountNameextend

Alsid user accounts issues

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

'Searches for triggered Indicators of Exposures related to user accounts issues'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: fb9e0b51-8867-48d7-86f4-6e76f2176bf8
name: Alsid user accounts issues
description: |
    'Searches for triggered Indicators of Exposures related to user accounts issues'
severity: Low
requiredDataConnectors:
  - connectorId: AlsidForAD
    dataTypes:
      - AlsidForADLog_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | 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

Let Binding: SeverityTable used in Stage 3

let SeverityTable = datatable(Severity:string,Level:int) [
"low", 1,
"medium", 2,
"high", 3,
"critical", 4
];

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-ACCOUNTS-DANG-SID-HISTORY", "C-PRE-WIN2000-ACCESS-MEMBERS", "C-PASSWORD-DONT-EXPIRE", "C-SLEEPING-ACCOUNTS", "C-DANG-PRIMGROUPID", "C-PASSWORD-NOT-REQUIRED", "C-USER-PASSWORD"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

AWSCloudTrail - RDS instance publicly exposed

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

Detects RDS instance creation or modification events where the database is configured as publicly accessible. Public exposure of database instances can increase the risk of unauthorized access and data leakage.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 8f1630c2-2e45-4df2-be43-50fba90f601d
name: AWSCloudTrail - RDS instance publicly exposed
description: |
  Detects RDS instance creation or modification events where the database is configured as publicly accessible.
  Public exposure of database instances can increase the risk of unauthorized access and data leakage.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1537
query: |
    AWSCloudTrail
    | where  EventName in ("CreateDBInstance", "ModifyDBInstance") and isempty(ErrorCode) and isempty(ErrorMessage)
    | where tostring(parse_json(RequestParameters).publiclyAccessible) == "true"
    | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
    | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
    | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
    | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
      AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  AWSRegion: AWSRegion
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS RDS instance publicly exposed by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} event from {{SourceIpAddress}} setting RDS publicly accessible in account {{RecipientAccountId}}.'
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where  EventName in ("CreateDBInstance", "ModifyDBInstance") and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: where

| where tostring(parse_json(RequestParameters).publiclyAccessible) == "true"

Stage 4: extend (4 consecutive steps)

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNamein
  • CreateDBInstance
  • ModifyDBInstance corpus 3 (panther 2, kusto 1)
field:"aws::eventName" kind:in
RequestParameters.publiclyAccessibleeq
  • true transforms: tostring, kusto_accessor_expr:{"expression":"tostring(parse_json(RequestParameters).publiclyAccessible)","field":"RequestParameters.publiclyAccessible"}, kusto_dynamic_path
field:"RequestParameters.publiclyAccessible" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

Azure secure score PW age policy new

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

'This query searches for having found that when periodic password resets are enforced, passwords become less secure. Users tend to pick a weaker password and vary it slightly for each reset. If a user creates a strong password (long, complex and without any pragmatic words present) it should remain just as strong in the future as it is today. It is Microsoft's official security position to not expire passwords periodically without a specific reason, and recommends that cloud-only tenants set the password policy to never expire.'

MITRE ATT&CK Coverage

Rule Body

id: 88C9A5E0-31EC-490B-82E5-A286D9B99A67
name: Azure secure score PW age policy new
description: |
  'This query searches for having found that when periodic password resets are enforced, passwords become less secure. Users tend to pick a weaker password and vary it slightly for each reset. 
   If a user creates a strong password (long, complex and without any pragmatic words present) it should remain just as strong in the future as it is today. It is Microsoft's official security position to not expire passwords periodically without a specific reason, and recommends that cloud-only tenants set the password policy to never expire.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SenservaPro
    dataTypes:
      - SenservaPro_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1555
  - T1606
  - T1040
query: |
  SenservaPro_CL
  | where ControlName_s == 'AzureSecureScorePWAgePolicyNew'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: ControlName_s
      - identifier: AadTenantId
        columnName: TenantId
      - identifier: DisplayName
        columnName: TenantDisplayName_s 
  - entityType: SecurityGroup
    fieldMappings:
      - identifier: DistinguishedName
        columnName: Group_s
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: SourceSystem 
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

SenservaPro_CL

Stage 2: where

| where ControlName_s == 'AzureSecureScorePWAgePolicyNew'

Indicators

These rows show field, operator, and value matches.

Beacon Traffic Based on Common User Agents Visiting Limited Number of Domains

#
Status
available
Severity
medium
Time window
7d
Group by
DestinationHostName, RequestClientApplication, SourceUserName
Source
github.com/Azure/Azure-Sentinel

This query searches web proxy logs for a specific type of beaconing behavior by joining a number of sources together: - Traffic by actual web browsers - by looking at traffic generated by a UserAgent that looks like a browser and is used by multiple users to visit a large number of domains. - Users that make requests using one of these actual browsers, but only to a small set of domains, none of which are common domains. - The traffic is beacon-like; meaning that it occurs during many different hours of the day (i.e. periodic).

MITRE ATT&CK Coverage

Rule Body

id: 6345c923-99eb-4a83-b11d-7af0ffa75577
name: Beacon Traffic Based on Common User Agents Visiting Limited Number of Domains
description: |
  This query searches web proxy logs for a specific type of beaconing behavior by joining a number of sources together: 
  - Traffic by actual web browsers - by looking at traffic generated by a UserAgent that looks like a browser and is used by multiple users to visit a large number of domains.
  - Users that make requests using one of these actual browsers, but only to a small set of domains, none of which are common domains.
  - The traffic is beacon-like; meaning that it occurs during many different hours of the day (i.e. periodic). 
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071.001
query: |
  let timeframe = 1d; // Timeframe during which to search for beaconing behavior.
  let lookback = 7d; // Look back period to find if browser was used for other domains by user.
  let min_requests=50; // Minimum number of requests to consider it beacon traffic.
  let min_hours=8; // Minimum number of different hours during which connections were made to consider it beacon traffic.
  let trusted_user_count=10; // If visited by this many users a domain is considered 'trusted'.
  let max_sites=3; // Maximum number of different sites visited using this user-agent.
  // Client-specific query to obtain 'browser-like' traffic from proxy logs.
  let BrowserTraffic = (p:timespan) {
  CommonSecurityLog
  | where DeviceVendor == "Zscaler" and DeviceProduct == "NSSWeblog"
  | where TimeGenerated >ago(p)
  | project TimeGenerated, SourceUserName, DestinationHostName, RequestClientApplication
  | where (RequestClientApplication startswith "Mozilla/" and RequestClientApplication contains "Gecko")
  };
  let CommonDomains = BrowserTraffic(timeframe)
  | summarize source_count=dcount(SourceUserName) by DestinationHostName
  | where source_count>trusted_user_count
  | project DestinationHostName;
  let CommonUA = BrowserTraffic(timeframe)
  | summarize source_count=dcount(SourceUserName), host_count=dcount(DestinationHostName) by RequestClientApplication
  | where source_count>trusted_user_count and host_count > 100 // Normal browsers are browsers used by many people and visiting many different sites.
  | project RequestClientApplication;
  // Find browsers that are common, i.e. many users use them and they use them to visit many different sites,
  // but some users only use the browser to visit a very limited set of sites.
  // These are considered suspicious, since they might be an attacker masquerading a beacon as a legitimate browser.
  let SuspiciousBrowers = BrowserTraffic(timeframe)
  | where RequestClientApplication in(CommonUA)
  | summarize BrowserHosts=make_set(DestinationHostName),request_count=count() by RequestClientApplication, SourceUserName
  | where array_length(BrowserHosts) <= max_sites and request_count >= min_requests
  | project RequestClientApplication, SourceUserName,BrowserHosts;
  // Just reporting on suspicious browsers gives too many false positives.
  // For example, users that have the browser open on the login screen of 1 specific application.
  // In the suspicious browsers we can search for 'beacon-like' behavior.
  // Get all browser traffic by the suspicious browsers.
  let PotentialAlerts=SuspiciousBrowers
  | join BrowserTraffic(timeframe) on RequestClientApplication, SourceUserName
  // Find beaconing-like traffic - i.e. contacting the same host in many different hours.
  | summarize hour_count=dcount(bin(TimeGenerated,1h)), BrowserHosts=any(BrowserHosts), request_count=count() by RequestClientApplication, SourceUserName, DestinationHostName
  | where hour_count >= min_hours and request_count >= min_requests
  // Remove common domains like login.microsoft.com.
  | join kind=leftanti CommonDomains on DestinationHostName
  | summarize RareHosts=make_set(DestinationHostName), TotalRequestCount=sum(request_count), BrowserHosts=any(BrowserHosts) by RequestClientApplication, SourceUserName
  // Remove browsers that visit any common domains.
  | where array_length(RareHosts) == array_length(BrowserHosts);
  // Look back for X days to see if the browser was not used to visit more hosts.
  // This is to get rid of someone that started up the browser a long time ago, and left only a single tab open.
  PotentialAlerts
  | join BrowserTraffic(lookback) on SourceUserName, RequestClientApplication
  | summarize RareHosts=any(RareHosts),BrowserHosts1d=any(BrowserHosts),BrowserHostsLookback=make_set(DestinationHostName) by SourceUserName, RequestClientApplication
  | where array_length(RareHosts) == array_length(BrowserHostsLookback)
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SourceUserName
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1d;
let lookback = 7d;
let min_requests = 50;
let min_hours = 8;
let trusted_user_count = 10;
let max_sites = 3;

let BrowserTraffic, let SuspiciousBrowers and let PotentialAlerts are inlined into the numbered stages below.

Let Binding: CommonDomains used in Stage 13

let CommonDomains = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName) by DestinationHostName
| where source_count>trusted_user_count
| project DestinationHostName;

Let Binding: CommonUA used in Stage 6

let CommonUA = BrowserTraffic(timeframe)
| summarize source_count=dcount(SourceUserName), host_count=dcount(DestinationHostName) by RequestClientApplication
| where source_count>trusted_user_count and host_count > 100
| project RequestClientApplication;

Stages 1 to 15 define let PotentialAlerts as the rule's main pipeline source. Stages 16 to 18 use it.

Stage 1: source

CommonSecurityLog

Stage 2: where

where DeviceProduct == "NSSWeblog" and DeviceVendor == "Zscaler"

Stage 3: where

where ...

Stage 4: project

project DestinationHostName, RequestClientApplication, SourceUserName, TimeGenerated

Stage 5: where

where RequestClientApplication contains "Gecko" and RequestClientApplication startswith "Mozilla/"

Stage 6: where

where RequestClientApplication == "CommonUA"

Stage 7: summarize

summarize BrowserHosts, request_count by RequestClientApplication, SourceUserName

Stage 8: where

where BrowserHosts <= 3 and request_count >= 50

Stage 9: project

project BrowserHosts, RequestClientApplication, SourceUserName

Stage 10: join

| join BrowserTraffic(timeframe) on RequestClientApplication, SourceUserName

Stage 11: summarize

summarize BrowserHosts, hour_count, request_count by RequestClientApplication, SourceUserName, DestinationHostName

Stage 12: where

where hour_count >= 8 and request_count >= 50

Stage 13: join (negated)

join kind=leftanti (CommonDomains) on DestinationHostName

Stage 14: summarize

summarize BrowserHosts, RareHosts, TotalRequestCount by RequestClientApplication, SourceUserName

Stage 15: where

where <unknown:array_length(, RareHosts)> =~ <unknown:array_length(, BrowserHosts)>

Stage 16: join

PotentialAlerts
| join BrowserTraffic(lookback) on SourceUserName, RequestClientApplication

Stage 17: summarize

summarize BrowserHosts1d, BrowserHostsLookback, RareHosts by SourceUserName, RequestClientApplication

Stage 18: where

where <unknown:array_length(, RareHosts)> =~ <unknown:array_length(, BrowserHostsLookback)>

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
BrowserHosts1dsummarize
BrowserHostsLookbacksummarize
RareHostssummarize
RequestClientApplicationsummarize
SourceUserNamesummarize

BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC3 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 - Non-Tier Zero Principals With ADCS ESC3 Privileges

Rule Body

id: e6c6fb5d-c2a4-4ff4-8c75-a66f37e1ecc0
name: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC3 Privileges
description: BloodHound Attack Path Finding - Non-Tier Zero Principals With ADCS ESC3
  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 == "T0ADCSESC3"
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 == "T0ADCSESC3"

Indicators

These rows show field, operator, and value matches.

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

Check Point Exposure Management - Alert Ingestion Anomaly

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

Detects when no Check Point Exposure Management alerts have been ingested into the argsentdc_CL table for an extended period. This may indicate a failure in the CCP data connector or the Importer playbook, a misconfigured API token, or network connectivity issues.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 8d3b9c7e-5a2f-4e1d-b6c8-3f9a7e2d1b4c
name: Check Point Exposure Management - Alert Ingestion Anomaly
description: |
  Detects when no Check Point Exposure Management alerts have been ingested into the argsentdc_CL table
  for an extended period. This may indicate a failure in the CCP data connector or the
  Importer playbook, a misconfigured API token, or network connectivity issues.
severity: Medium
queryFrequency: 1h
queryPeriod: 24h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
requiredDataConnectors:
  - connectorId: CheckPointCyberintAlerts
    dataTypes:
      - argsentdc_CL
query: |
  let lookback = 24h;
  let expected_interval = 1h;
  let last_record = toscalar(
      argsentdc_CL
      | where TimeGenerated > ago(lookback)
      | summarize max(TimeGenerated)
  );
  let gap_minutes = datetime_diff('minute', now(), last_record);
  // Alert if no records in the last expected_interval, but there WERE records in the lookback period
  // (avoids alerting on brand-new deployments with no data yet)
  argsentdc_CL
  | where TimeGenerated > ago(lookback)
  | summarize
      LastIngestionTime = max(TimeGenerated),
      TotalRecords = count(),
      DistinctAlerts = dcount(ref_id)
  | where LastIngestionTime < ago(expected_interval)
  | extend GapMinutes = datetime_diff('minute', now(), LastIngestionTime)
  | project
      LastIngestionTime,
      GapMinutes,
      TotalRecords,
      DistinctAlerts,
      Message = strcat("No Check Point Exposure Management alerts ingested for ", GapMinutes, " minutes. Last record at ", LastIngestionTime)
suppressionEnabled: false
suppressionDuration: 5h
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 24h
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDescriptionFormat: '{{Message}}'
  alertDisplayNameFormat: 'Check Point Exposure Management - Ingestion gap detected ({{GapMinutes}} minutes)'
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 24h;
let expected_interval = 1h;
let gap_minutes = datetime_diff('minute', now(), last_record);

Let Binding: last_record

let last_record = toscalar(
    argsentdc_CL
    | where TimeGenerated > ago(lookback)
    | summarize max(TimeGenerated)
);

Stage 1: source

argsentdc_CL

Stage 2: where

| where TimeGenerated > ago(lookback)

Stage 3: summarize

| summarize
    LastIngestionTime = max(TimeGenerated),
    TotalRecords = count(),
    DistinctAlerts = dcount(ref_id)

Stage 4: where

| where LastIngestionTime < ago(expected_interval)

Stage 5: extend

| extend GapMinutes = datetime_diff('minute', now(), LastIngestionTime)

Stage 6: project

| project
    LastIngestionTime,
    GapMinutes,
    TotalRecords,
    DistinctAlerts,
    Message = strcat("No Check Point Exposure Management alerts ingested for ", GapMinutes, " minutes. Last record at ", LastIngestionTime)

Output Fields

These fields are emitted when the rule matches.

FieldSource
DistinctAlertsproject
GapMinutesproject
LastIngestionTimeproject
Messageproject
TotalRecordsproject

Cognni Incidents for Low Sensitivity HR Information

#

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

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

'Display incidents in which low sensitive HR information was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: ef8654b1-b2cf-4f6c-ae5c-eca635a764e8
name: Cognni Incidents for Low Sensitivity HR Information
description: |
  'Display incidents in which low sensitive HR information was placed at risk by user sharing.'
severity: Low
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let lowRisk = 1;
  let hr = 'HR Information';
  CognniIncidents_CL 
  | where Severity == lowRisk
  | where informationType_s == hr
  | where TimeGenerated >= ago(5h)
  | extend AccountCustomEntity = userId_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lowRisk = 1;
let hr = 'HR Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == lowRisk

Stage 3: where

| where informationType_s == hr

Stage 4: where

| where TimeGenerated >= ago(5h)

Stage 5: extend

| extend AccountCustomEntity = userId_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

CYFIRMA - Data Breach and Web Monitoring - Ransomware Exposure Detected 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 analytics rule detects high-severity ransomware threats targeting the organization, as reported by CYFIRMA's Dark Web and Data Breach Intelligence feeds. The alert is generated when threat actors post, claim, or associate ransomware activity with corporate domains, brands, or subsidiaries, indicating a potential data breach, extortion attempt, or unauthorized access."

MITRE ATT&CK Coverage

Rule Body

id: ed1aabc1-e1c1-42f4-abac-fd5637730f13
name: CYFIRMA - Data Breach and Web Monitoring - Ransomware Exposure Detected Rule
description: |
  "This analytics rule detects high-severity ransomware threats targeting the organization, as reported by CYFIRMA's Dark Web and Data Breach Intelligence feeds. 
  The alert is generated when threat actors post, claim, or associate ransomware activity with corporate domains, brands, or subsidiaries, indicating a potential data breach, extortion attempt, or unauthorized access."
version: 1.0.1
kind: Scheduled
severity: High
requiredDataConnectors:
  - connectorId: CyfirmaDigitalRiskAlertsConnector
    dataTypes:
      - CyfirmaDBWMRansomwareAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1566.001
  - T1566.002
  - T1566.003
query: |
  // High severity - Data Breach and Web Monitoring - Ransomware Exposure Detected
  let timeFrame = 5m;
  CyfirmaDBWMRansomwareAlerts_CL
  | where severity == 'Critical' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=signature,
      Source=source,
      Impact=impact,
      Recommendation='',
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT',
      AlertTitle=Alert_title,
      ThreatActors= threat_actors
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Source,
      Impact,
      Recommendation,
      ProductName,
      ProviderName,
      AlertTitle,
      ThreatActors
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  ThreatActors: ThreatActors
  RiskScore: RiskScore
  Source: Source
  Impact: Impact
  Description: Description
  Recommendation: Recommendation
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  TimeGenerated: TimeGenerated
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - High Severity Alert: Ransomware Exposure Detected - {{AlertTitle}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaDBWMRansomwareAlerts_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,
    AlertUID=alert_uid,
    UID=uid,
    AssetType=asset_type,
    AssetValue=signature,
    Source=source,
    Impact=impact,
    Recommendation='',
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT',
    AlertTitle=Alert_title,
    ThreatActors= threat_actors

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

CYFIRMA - Medium severity TOR Node Network Indicators - Block Recommended Rule

#

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

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

"This KQL query identifies network-based indicators from CYFIRMA intelligence that are associated with the role 'TOR'. These indicators may include IP addresses, domains, and URLs related to Tor network activity. Threat actors often use Tor for anonymous communication, command and control, data exfiltration, and evasion of network defenses."

MITRE ATT&CK Coverage

Rule Body

id: aba36dc3-af43-4ab6-9349-3d1e37f1d4f3
name: CYFIRMA - Medium severity TOR Node Network Indicators - Block Recommended Rule
description: |
  "This KQL query identifies network-based indicators from CYFIRMA intelligence that are associated with the role 'TOR'. 
  These indicators may include IP addresses, domains, and URLs related to Tor network activity. 
  Threat actors often use Tor for anonymous communication, command and control, data exfiltration, and evasion of network defenses."
version: 1.0.1
kind: Scheduled
severity: Medium
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  //TOR Node Network Indicators - Block Recommended
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where (ConfidenceScore < 80 and ConfidenceScore >= 50)
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and Roles has 'TOR'
  | extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
  | extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
  | extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
  | extend parsed = parse_json(extensions)
  | extend extensionKeys = bag_keys(parsed)
  | mv-expand extensionKeys
  | extend extensionKeyStr = tostring(extensionKeys)
  | extend ext = parsed[extensionKeyStr]
  | extend props = ext.properties
  | extend 
      extension_id = extensionKeyStr,
      ASN_Owner = props.asn_owner,
      ASN = props.asn,
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project
      IPv4,
      IPv6,
      URL,
      Domain,
      ThreatActors,
      RecommendedActions,
      Sources,
      Roles,
      Country,
      IPAbuse,
      name,
      Description,
      ConfidenceScore,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      SecurityVendors,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - CommandAndControl
  - Exfiltration
  - InitialAccess
  - Persistence
  - Reconnaissance
relevantTechniques:
  - T1090
  - T1572
  - T1048
  - T1071
  - T1189
  - T1505
  - T1595
  - T1090.003
  - T1048.002
  - T1071.001
  - T1505.003
  - T1595.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence TOR Node Network Indicators - Block Recommended - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  Created: created
  Modified: modified
  ValidFrom: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
  IPAbuse: IPAbuse
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv4
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPv6
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaIndicators_CL

Stage 2: where

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

Stage 3: extend (6 consecutive steps)

| extend IPv4 = extract(@"ipv4-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend IPv6 = extract(@"ipv6-addr:value\s*=\s*'([^']+)'", 1, pattern)
| extend URL = extract(@"url:value\s*=\s*'([^']+)'", 1, pattern)
| extend Domain = extract(@"domain-name:value\s*=\s*'([^']+)'", 1, pattern)
| extend parsed = parse_json(extensions)
| extend extensionKeys = bag_keys(parsed)

Stage 4: mv-expand

| mv-expand extensionKeys

Stage 5: extend (4 consecutive steps)

| extend extensionKeyStr = tostring(extensionKeys)
| extend ext = parsed[extensionKeyStr]
| extend props = ext.properties
| extend 
    extension_id = extensionKeyStr,
    ASN_Owner = props.asn_owner,
    ASN = props.asn,
    ProviderName = 'CYFIRMA',
    ProductName = 'DeCYFIR/DeTCT'

Stage 6: project

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

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

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

DDoS Attack IP Addresses - Percent Threshold

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

Identifies IP addresses that generate over 5% of traffic during DDoS attack mitigation

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 402a42ad-f31c-48d1-8f80-0200846b7f25
kind: Scheduled
name: DDoS Attack IP Addresses - Percent Threshold
description: Identifies IP addresses that generate over 5% of traffic during DDoS attack mitigation
severity: Medium
status : Available
requiredDataConnectors:
  - connectorId: DDOS
    dataTypes:
      - AzureDiagnostics
queryPeriod: 2h
queryFrequency: 2h
triggerOperator: gt
triggerThreshold: 0
query: |
  let T1 = AzureDiagnostics
    | where ResourceType == "PUBLICIPADDRESSES" and Category == "DDoSMitigationFlowLogs"
    | summarize rows_count = count() by destPublicIpAddress_s, sourcePublicIpAddress_s;
  let T2 = AzureDiagnostics
    | where ResourceType == "PUBLICIPADDRESSES" and Category == "DDoSMitigationFlowLogs"
    | summarize rows_total = count() by destPublicIpAddress_s;
  T2
    | join kind=leftouter T1 on destPublicIpAddress_s
    | project destPublicIpAddress_s, sourcePublicIpAddress_s, percent_of_traffic = 100*rows_count/rows_total
    | order by percent_of_traffic desc
    | where percent_of_traffic > 5
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: sourcePublicIpAddress_s
tactics:
  - Impact
relevantTechniques:
  - T1498
version: 1.0.1

Stages and Predicates

let T2 is inlined into the numbered stages below.

Let Binding: T1 used in Stage 4

let T1 = AzureDiagnostics
  | where ResourceType == "PUBLICIPADDRESSES" and Category == "DDoSMitigationFlowLogs"
  | summarize rows_count = count() by destPublicIpAddress_s, sourcePublicIpAddress_s;

Stages 1 to 3 define let T2 as the rule's main pipeline source. Stages 4 to 7 use it.

Stage 1: source

AzureDiagnostics

Stage 2: where

| where ResourceType == "PUBLICIPADDRESSES" and Category == "DDoSMitigationFlowLogs"

Stage 3: summarize

| summarize rows_total = count() by destPublicIpAddress_s

Stage 4: join

T2
| join kind=leftouter T1 on destPublicIpAddress_s

Stage 5: project

project destPublicIpAddress_s, percent_of_traffic, sourcePublicIpAddress_s

Stage 6: sort

sort by percent_of_traffic

Stage 7: where

where percent_of_traffic > 5

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
destPublicIpAddress_sproject
percent_of_trafficproject
sourcePublicIpAddress_sproject

Detect suspicious foci token logins V2

#
Group by
SessionId, UserPrincipalName
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

FOCI tokens (Family of Client IDs tokens) are special refresh tokens that allow multiple applications within the same "family" to share authentication tokens. This means that once a user authenticates with one application, they can access other applications in the same family without needing to re-authenticate. For adversaries, these are very interesting tokens to abuse since they can access a normal application (Microsoft Teams for example), and reuse that refresh token to access another application (like Azure CLI). To detect a suspicious foci token combination, we look for all the logins using foci tokens and group them by Session ID (since these belong to the same session). Then we take the first login where no refresh token was provided, and look at the logins that used refresh tokens as incomming token types within that same session. If the second login application is one that is typically abused by adversaries and the application for the first login is a 'normal' application, we flag the event. This version is the V2 version for this query in this repo, which also flags when an adversary is using the same application to get new access tokens but with another scope (compared to V1 which does not do this). The v2 version focusses more on RoadTool detection tho, while the v1 detection is more broad. Some organizations have a high BP hit count on Microsoft Azure CLI. To limit those hits, you have three finetune options to enable in the query: - Only alert when first and second login has X time between each other (default 90 minutes if enabled) - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token - Only alert on Microsoft Azure CLI when Global Administrator scope is used in token and request came from a non-compliant device

MITRE ATT&CK Coverage

TacticTechniques
Execution
Credential Access

References

Telemetry Coverage

Rule Body

// TimeDiff threshold in minutes. Needed for some environments with a lot of BP hits on long time frames. Used in scenario where you expect adversary to quickly request new tokens after first token request.
let maxTimeDiff = 90;
// External lookup to get list of FOCI applications
let FociClientApplications = toscalar(externaldata(client_id: string)
    [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true)
    //| project-rename FociClientId = client_id
    | summarize FociClientId = make_list(client_id)
    );
// Get all token requests for Foci clients
let FociTokenRequest = materialize (
    AADNonInteractiveUserSignInLogs
    | where TimeGenerated > ago(6h)
    // Filter for sign-ins to home tenant only
    | where HomeTenantId == ResourceTenantId
    // Lookup for FOCI client
    | where AppId in (FociClientApplications)
    );
FociTokenRequest
// First get all initial logins without refresh tokens as incomming token type
| where IncomingTokenType == "none"
// Then get logins with refresh tokens for same session
| join kind=inner (
    FociTokenRequest
    | where IncomingTokenType != "none"
    | project-rename
        SecondAppDisplayName = AppDisplayName,
        SecondRequestTimeGenerated = TimeGenerated,
        SecondAppId = AppId
    )
    on SessionId, UserPrincipalName
| extend FirstOauthScopeInfo = extract("{\"key\":\"Oauth Scope Info\",\"value\":\"\\[(.*)\\]\"}", 1, AuthenticationProcessingDetails),
    SecondOauthScopeInfo = extract("{\"key\":\"Oauth Scope Info\",\"value\":\"\\[(.*)\\]\"}", 1, AuthenticationProcessingDetails1)
// Only get requests where refresh token was used after first sign-in
| extend TimeDiff = datetime_diff('minute', SecondRequestTimeGenerated, TimeGenerated)
| where TimeDiff >= 1 and TimeDiff <= maxTimeDiff
// Only project needed columns
| project
    FirstRequestTimeGenerated = TimeGenerated,
    FirstResult = ResultType,
    FirstResultDescription = ResultDescription,
    Identity,
    Location,
    FirstAppDisplayName = AppDisplayName,
    FirstAppId = AppId,
    ClientAppUsed,
    DeviceDetail,
    SecondDeviceDetail = DeviceDetail1,
    IPAddress,
    LocationDetails,
    UserAgent,
    SecondRequestTimeGenerated,
    SecondResult = ResultType,
    SecondResultDescription = ResultDescription1,
    SecondAppDisplayName,
    SecondAppId,
    SeconIncomingTokenType = IncomingTokenType1,
    SessionId,
    TimeDiff,
    FirstOauthScopeInfo,
    SecondOauthScopeInfo,
    FirstResourceIdentity = ResourceIdentity,
    SecondResourceIdentity = ResourceIdentity1
// Flag logins to the following applications as second login, since these are the most popular used for RoadTools (fico apps, localhost redirect URI, and Azure AD resource identity)
| where SecondAppDisplayName in ("Microsoft Azure CLI", "Copilot App", "Microsoft Azure PowerShell", "Visual Studio - Legacy", "Microsoft Edge Enterprise New Tab Page") and SecondResourceIdentity == "00000002-0000-0000-c000-000000000000"
| where SecondResult == 0
// ENVIRONMENT SPECIFIC FINETUNING - BEGIN
// Most BP triggers are mainly on Microsoft Azure CLI, so we provide two ways of handling these BP detections (strongly depends on environment)
// OPTION 1 - Flag login to Azure CLI using 'Global Administrator' ID in token scope
//| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10"))
// OPTION 2 - Flag login to Azure CLI using 'Global Administrator' ID in token scope from non compliant device
//| where (SecondAppDisplayName in ("Microsoft Azure PowerShell", "Office 365 Management") or (SecondAppDisplayName == "Microsoft Azure CLI" and SecondAuthenticationProcessingDetails contains "62e90394-69f5-4237-9190-012177145e10" and todynamic(SecondDeviceDetail).isCompliant != "true"))
// ENVIRONMENT SPECIFIC FINETUNING - END

Stages and Predicates

Parameters

let maxTimeDiff = 90;

let FociTokenRequest is inlined into the numbered stages below.

Let Binding: FociClientApplications used in Stage 4

let FociClientApplications = toscalar(externaldata(client_id: string)
    [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true)
    | summarize FociClientId = make_list(client_id)
    );

Stages 1 to 4 define let FociTokenRequest as the rule's main pipeline source. Stages 5 to 12 use it.

Stage 1: source

AADNonInteractiveUserSignInLogs

Stage 2: where

| where TimeGenerated > ago(6h)

Stage 3: where

| where HomeTenantId == ResourceTenantId

Stage 4: where

| where AppId in (FociClientApplications)

Stage 5: where

FociTokenRequest
| where IncomingTokenType == "none"

Stage 6: join

| join kind=inner (
    FociTokenRequest
    | where IncomingTokenType != "none"
    | project-rename
        SecondAppDisplayName = AppDisplayName,
        SecondRequestTimeGenerated = TimeGenerated,
        SecondAppId = AppId
    )
    on SessionId, UserPrincipalName

Stage 7: extend

extend FirstOauthScopeInfo, SecondOauthScopeInfo

Stage 8: extend

extend TimeDiff

Stage 9: where

where TimeDiff >= 1 and TimeDiff <= 90

Stage 10: project

project ClientAppUsed, DeviceDetail, FirstAppDisplayName, FirstAppId, FirstOauthScopeInfo, FirstRequestTimeGenerated, FirstResourceIdentity, FirstResult, FirstResultDescription, IPAddress, Identity, Location, LocationDetails, SeconIncomingTokenType, SecondAppDisplayName, SecondAppId, SecondDeviceDetail, SecondOauthScopeInfo, SecondRequestTimeGenerated, SecondResourceIdentity, SecondResult, SecondResultDescription, SessionId, TimeDiff, UserAgent

Stage 11: where

where SecondAppDisplayName in ("Copilot App", "Microsoft Azure CLI", "Microsoft Azure PowerShell", "Microsoft Edge Enterprise New Tab Page", "Visual Studio - Legacy") and SecondResourceIdentity == "00000002-0000-0000-c000-000000000000"

Stage 12: where

where SecondResult == 0

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientAppUsedproject
DeviceDetailproject
FirstAppDisplayNameproject
FirstAppIdproject
FirstOauthScopeInfoproject
FirstRequestTimeGeneratedproject
FirstResourceIdentityproject
FirstResultproject
FirstResultDescriptionproject
IPAddressproject
Identityproject
Locationproject
LocationDetailsproject
SeconIncomingTokenTypeproject
SecondAppDisplayNameproject
SecondAppIdproject
SecondDeviceDetailproject
SecondOauthScopeInfoproject
SecondRequestTimeGeneratedproject
SecondResourceIdentityproject
SecondResultproject
SecondResultDescriptionproject
SessionIdproject
TimeDiffproject
UserAgentproject

Detect Windows Allow Firewall Rule Addition/Modification

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

This analytic rule detects any registry value creation or modification of Windows firewall registry keys to allow network traffic. This could be an indication of defense evasion by an adversary to allow network traffic to/from a compromised host.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 056593d4-ca3b-47a7-be9d-d1d0884a1d36
name: Detect Windows Allow Firewall Rule Addition/Modification
description: |
  This analytic rule detects any registry value creation or modification of Windows firewall registry keys to allow network traffic. This could be an indication of defense evasion by an adversary to allow network traffic to/from a compromised host.
severity: Medium
status: Available 
tags:
  - Schema: _ASim_RegistryEvent
    SchemaVersion: 0.1.2
requiredDataConnectors:
  - connectorId: CrowdStrikeFalconEndpointProtection
    dataTypes: 
      - CommonSecurityLog
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - SecurityAlert
  - connectorId: SentinelOne
    dataTypes:
      - SentinelOne_CL
  - connectorId: VMwareCarbonBlack
    dataTypes:
      - CarbonBlackEvents_CL
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint_CL
  - connectorId: TrendMicroApexOne
    dataTypes:
      - TMApexOneEvent
  - connectorId: TrendMicroApexOneAma
    dataTypes:
      - TMApexOneEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  // List of Windows Firewall registry keys to monitor
  let firewallRegistryList = dynamic([
        'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\RestrictedServices\\Static\\System',
        'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\RestrictedServices\\Configurable\\System',
        'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Defaults\\FirewallPolicy\\FirewallRules',
        'HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\WindowsFirewall'
    ]);
  _ASim_RegistryEvent
  | where EventType in ('RegistryValueSet', 'RegistryKeyCreated') 
  | where RegistryKey has_any (firewallRegistryList) and RegistryValueData has_all ('Action=Allow', 'Active=TRUE')
  | project
      TimeGenerated,
      DvcHostname,
      ActorUsername,
      ActorUsernameType,
      ActingProcessId,
      ActingProcessName,
      ActingProcessCommandLine,
      RegistryKey,
      RegistryValue,
      RegistryValueType,
      RegistryValueData
  | extend HostName = tostring(split(DvcHostname, '.')[0])
  | extend DnsDomain = tostring(strcat_array(array_slice(split(DvcHostname, '.'), 1, -1), '.'))
  | extend Username = iff(tostring(ActorUsernameType) == 'Windows', tostring(split(ActorUsername, '\\')[1]), ActorUsername)
  | extend NTDomain = iff(tostring(ActorUsernameType) == 'Windows', tostring(split(ActorUsername, '\\')[0]), ActorUsername)
  | extend Username = iff(tostring(ActorUsernameType) == 'UPN', tostring(split(ActorUsername, '@')[0]), Username)
  | extend UPNSuffix = iff(tostring(ActorUsernameType) == 'UPN', tostring(split(ActorUsername, '@')[1]), '')
  | extend RegHive = tostring(split(RegistryKey, '\\')[0]), RegKey = tostring(strcat_array(array_slice(split(RegistryKey, '\\'), 1, -1), '\\')) 
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Username
      - identifier: UPNSuffix
        columnName: UPNSuffix
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ActingProcessId
      - identifier: CommandLine
        columnName: ActingProcessCommandLine
  - entityType: RegistryKey
    fieldMappings:
      - identifier: Hive
        columnName: RegHive
      - identifier: Key
        columnName: RegKey
  - entityType: RegistryValue
    fieldMappings:
      - identifier: Name
        columnName: RegistryValue
      - identifier: Value
        columnName: RegistryValueData
      - identifier: ValueType
        columnName: RegistryValueType
eventGroupingSettings:
  aggregationKind: singleAlert
alertDetailsOverride:
  alertDisplayNameFormat: "Allow Firewall Rule {{RegistryValueData}} added at registry key {{RegKey}} on {{HostName}}"
  alertDescriptionFormat: "An allow Firewall Rule {{RegistryValueData}} added at registry key {{RegKey}} by {{Username}}."
version: 1.0.0
kind: Scheduled

Stages and Predicates

Let Binding: firewallRegistryList used in Stage 3

let firewallRegistryList = dynamic([
      'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\RestrictedServices\\Static\\System',
      'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Parameters\\FirewallPolicy\\RestrictedServices\\Configurable\\System',
      'HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\SharedAccess\\Defaults\\FirewallPolicy\\FirewallRules',
      'HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\WindowsFirewall'
  ]);

Stage 1: source

_ASim_RegistryEvent

Stage 2: where

| where EventType in ('RegistryValueSet', 'RegistryKeyCreated')

Stage 3: where

| where RegistryKey has_any (firewallRegistryList) and RegistryValueData has_all ('Action=Allow', 'Active=TRUE')

Stage 4: project

| project
    TimeGenerated,
    DvcHostname,
    ActorUsername,
    ActorUsernameType,
    ActingProcessId,
    ActingProcessName,
    ActingProcessCommandLine,
    RegistryKey,
    RegistryValue,
    RegistryValueType,
    RegistryValueData

Stage 5: extend (7 consecutive steps)

| extend HostName = tostring(split(DvcHostname, '.')[0])
| extend DnsDomain = tostring(strcat_array(array_slice(split(DvcHostname, '.'), 1, -1), '.'))
| extend Username = iff(tostring(ActorUsernameType) == 'Windows', tostring(split(ActorUsername, '\\')[1]), ActorUsername)
| extend NTDomain = iff(tostring(ActorUsernameType) == 'Windows', tostring(split(ActorUsername, '\\')[0]), ActorUsername)
| extend Username = iff(tostring(ActorUsernameType) == 'UPN', tostring(split(ActorUsername, '@')[0]), Username)
| extend UPNSuffix = iff(tostring(ActorUsernameType) == 'UPN', tostring(split(ActorUsername, '@')[1]), '')
| extend RegHive = tostring(split(RegistryKey, '\\')[0]), RegKey = tostring(strcat_array(array_slice(split(RegistryKey, '\\'), 1, -1), '\\'))

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypein
  • RegistryKeyCreated corpus 5 (kusto 5)
  • RegistryValueSet corpus 5 (kusto 5)
field:"EventType" kind:in
RegistryKeymatch
  • HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall transforms: term
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Defaults\FirewallPolicy\FirewallRules transforms: term
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\RestrictedServices\Configurable\System transforms: term
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\RestrictedServices\Static\System transforms: term
field:"TargetObject" kind:match
RegistryValueDatamatch
  • Action=Allow transforms: term
  • Active=TRUE transforms: term
field:"Details" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActingProcessCommandLineproject
ActingProcessIdproject
ActingProcessNameproject
ActorUsernameproject
ActorUsernameTypeproject
DvcHostnameproject
RegistryKeyproject
RegistryValueproject
RegistryValueDataproject
RegistryValueTypeproject
TimeGeneratedproject
HostNameextend
DnsDomainextend
Usernameextend
NTDomainextend
UPNSuffixextend
RegHiveextend
RegKeyextend

Detection Opportunities for Certighost (CVE-2026-54121)

#

Telemetry Coverage

Rule Body

DeviceNetworkEvents
| where ActionType == "ListeningConnectionCreated"
| where LocalPort == 389
| where DeviceName !in~ ("list of domain controllers")

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://academy.bluraven.io)
//
let domain_controllers = pack_array("list of domain controllers");
let domain_controller_ips = pack_array("IP addresses of domain controllers");
DeviceLogonEvents
| where AccountName has_any (domain_controllers)
| where isnotempty(RemoteIP)
| where RemoteIP !in ("::1", "127.0.0.1", "-")
| where RemoteIP !startswith "fe80::"
| where RemoteIP !in (domain_controller_ips)

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://academy.bluraven.io)
//
let domain_controllers = pack_array("list of domain controllers");
let domain_controller_ips = pack_array("IP addresses of domain controllers");
SecurityEvent
| where EventID == 4624
| where TargetUserName has_any (domain_controllers)
| where isnotempty(IpAddress)
| where IpAddress !in ("::1", "127.0.0.1", "-")
| where IpAddress !startswith "fe80::"
| where IpAddress !in (domain_controller_ips)
| project-reorder TimeGenerated, Computer, TargetUserName, IpAddress

let domain_controller_fqdns = pack_array("list of domain controller fqdns");
let domain_controller_ips = pack_array("IP addresses of domain controllers");
SecurityEvent
| where EventID in (4886, 4887, 4888)
| extend cdc = extract(@'cdc:(\S+)', 1, Attributes), rmd = extract(@'rmd:(\S+)', 1, Attributes)
| where not (cdc in~ (domain_controller_ips) or cdc in~ (domain_controller_fqdns))

Stages and Predicates

Stage 1: source

DeviceNetworkEvents

Stage 2: where

where ActionType == "ListeningConnectionCreated"

Stage 3: where

where LocalPort == 389

Stage 4: where

where not (DeviceName =~ "list of domain controllers")

Stage 5: source

DeviceLogonEvents

Stage 6: where

where AccountName has "domain_controllers"

Stage 7: where

where isnotempty(RemoteIP)

Stage 8: where

where not (RemoteIP in ("-", "127.0.0.1", "::1"))

Stage 9: where

where not (RemoteIP startswith "fe80::")

Stage 10: where

where not (RemoteIP == "domain_controller_ips")

Stage 11: source

SecurityEvent

Stage 12: where

where EventID == 4624

Stage 13: where

where TargetUserName has "domain_controllers"

Stage 14: where

where isnotempty(IpAddress)

Stage 15: where

where not (IpAddress in ("-", "127.0.0.1", "::1"))

Stage 16: where

where not (IpAddress startswith "fe80::")

Stage 17: where

where not (IpAddress == "domain_controller_ips")

Stage 18: project-reorder

project-reorder

Stage 19: source

SecurityEvent

Stage 20: where

where EventID in (4886, 4887, 4888)

Stage 21: extend

extend cdc, rmd

Stage 22: where

where not ((cdc =~ "domain_controller_fqdns" or cdc =~ "domain_controller_ips"))

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

KnowBe4 Defend - Dangerous Attachment Detected

#
Status
available
Severity
medium
Time window
30m
Group by
Account_0_FullName, From, SenderIP, Subject, TimeGenerated, timesClicked
Source
github.com/Azure/Azure-Sentinel

'Defend has detected a user has a suspicious file type from a suspicious sender in their mailbox.'

MITRE ATT&CK Coverage

Rule Body

id: b75a4a7a-dedb-47be-b7e6-344f4d50af19
name: KnowBe4 Defend - Dangerous Attachment Detected
description: |
  'Defend has detected a user has a suspicious file type from a suspicious sender in their mailbox.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: KnowBe4Defend
    datatypes:
      - KnowBe4Defend_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - InitialAccess
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1204
  - T0853
  - T0863
  - T1566
  - T1546
  - T1546
tags:
  - Defend
query: |
  DefendAuditData
  | where ThreatLevel == "suspicious" or ThreatLevel == "dangerous"
  | mv-expand todynamic(Attachments)
  | where Attachments.name matches regex @"(?i)^.*\.(doc|docx|docm|pdf|xls|xlsx|xlsm|html|zip)$(?-i)"
  | summarize attachmentCount=count() by TimeGenerated, tostring(Attachments.name), Subject, From, Account_0_FullName = trim(@"[^@.\w]+",Recipients), timesClicked = LinksClicked, SenderIP
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account_0_FullName
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: Attachments_name
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: Account_0_FullName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SenderIP
alertDetailsOverride:
  alertDisplayNameFormat: Alert - {{Account_0_FullName}} has suspicious attachment.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DefendAuditData

Stage 2: where

| where ThreatLevel == "suspicious" or ThreatLevel == "dangerous"

Stage 3: mv-expand

| mv-expand todynamic(Attachments)

Stage 4: where

| where Attachments.name matches regex @"(?i)^.*\.(doc|docx|docm|pdf|xls|xlsx|xlsm|html|zip)$(?-i)"

Stage 5: summarize

| summarize attachmentCount=count() by TimeGenerated, tostring(Attachments.name), Subject, From, Account_0_FullName = trim(@"[^@.\w]+",Recipients), timesClicked = LinksClicked, SenderIP

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Attachments.nameregex_match
  • (?i)^.*.(doc|docx|docm|pdf|xls|xlsx|xlsm|html|zip)$(?-i) transforms: kusto_dynamic_path
field:"Attachments.name" kind:regex_match value:"(?i)^.*.(doc|docx|docm|pdf|xls|xlsx|xlsm|html|zip)$(?-i)"
ThreatLeveleq
  • dangerous
  • suspicious
field:"ThreatLevel" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
Account_0_FullNamesummarize
Fromsummarize
SenderIPsummarize
Subjectsummarize
TimeGeneratedsummarize
attachmentCountsummarize
timesClickedsummarize

Netskope - WebTransaction Error Detection

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

'Rule helps to track error occurred in Netskope WebTransaction Data Connector.'

MITRE ATT&CK Coverage

TacticTechniques
Execution

Rule Body

id: "66c4cd4c-d391-47e8-b4e6-93e55d86ca9f"
name: "Netskope - WebTransaction Error Detection"
description: |
  'Rule helps to track error occurred in Netskope WebTransaction Data Connector.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: NetskopeDataConnector
    dataTypes:
      - NetskopeWebtxErrors_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Execution
relevantTechniques:
  - T1204
query: |
    NetskopeWebtxErrors_CL
    |where error_s has_any ("Invalid Netskope Hostname", "Webtx Authentication", "Webtx Token Empty", "Webtx Exponential Backoff", "Webtx Idle Time")
incidentConfiguration:
    createIncident: true
    groupingConfiguration : 
      enabled: true
      reopenClosedIncident: false
      lookbackDuration : 5m
      matchingMethod : AnyAlert
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: 'Netskope Error at {{TimeGenerated}}'
    alertDescriptionFormat: 'Error Message: {{error_s}}'
customDetails:
    ErrorMessage: error_s
    Time: TimeGenerated
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

NetskopeWebtxErrors_CL

Stage 2: where

| where error_s has_any ("Invalid Netskope Hostname", "Webtx Authentication", "Webtx Token Empty", "Webtx Exponential Backoff", "Webtx Idle Time")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
error_smatch
  • Invalid Netskope Hostname transforms: term
  • Webtx Authentication transforms: term
  • Webtx Exponential Backoff transforms: term
  • Webtx Idle Time transforms: term
  • Webtx Token Empty transforms: term
field:"error_s" kind:match

Objects for Protection Group Deleted

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

Detects when objects are deleted from a protection group. This might indicate unauthorized removal of critical components.

Rule Body

id: 88a61215-b3e6-4c78-8acd-9078d9bcfdc3
name: Objects for Protection Group Deleted
description: Detects when objects are deleted from a protection group. This might
  indicate unauthorized removal of critical components.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 29150\n| extend ProtectionGroupName\
  \ = extract(\"ProtectionGroupName=\\\"([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n\
  \    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource\
  \ = original_host,\n    EventId = instanceId,\n    [\"User Name\"] = user,\n   \
  \ [\"Protection Group Name\"] = ProtectionGroupName,\n    MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 29150

Stage 3: extend

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

Stage 4: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    ["User Name"] = user,
    ["Protection Group Name"] = ProtectionGroupName,
    MessageDetails = Description,
   Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Protection Group Nameproject
Severityproject
User Nameproject

Oracle - Malicious user agent

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

'Detects known malicious user agent strings'

MITRE ATT&CK Coverage

Rule Body

id: 51d050ee-5740-11ec-bf63-0242ac130002
name: Oracle - Malicious user agent
description: |
  'Detects known malicious user agent strings'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - OracleWebLogicServer_CL
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  OracleWebLogicServerEvent
  | where isnotempty(HttpUserAgentOriginal)
  | where HttpUserAgentOriginal has_any ('Nikto', '(hydra)', '.nasl', 'absinthe', 'advanced email extractor', 'arachni/', 'autogetcontent', 'bilbo', 'BFAC', 'brutus', 'brutus/aet', 'bsqlbf', 'cgichk', 'cisco-torch', 'commix', 'core-project/1.0', 'crimscanner/', 'datacha0s', 'dirbuster', 'domino hunter', 'dotdotpwn', 'email extractor', 'fhscan core 1.', 'floodgate', 'get-minimal', 'gootkit auto-rooter scanner', 'grabber', 'grendel-scan', 'havij', 'inspath', 'internet ninja', 'jaascois', 'zmeu', 'masscan', 'metis', 'morfeus', 'mysqloit', 'n-stealth', 'nessus', 'netsparker', 'nmap nse', 'nmap scripting engine', 'nmap-nse', 'nsauditor', 'openvas', 'pangolin', 'paros', 'pmafind', 'prog.customcrawler', 'qualys was', 's.t.a.l.k.e.r.', 'security scan', 'springenwerk', 'sql power injector', 'sqlmap', 'sqlninja', 'teh forest lobster', 'this is an exploit', 'toata dragostea', 'toata dragostea mea pentru diavola', 'uil2pn', 'user-agent:', 'vega/', 'voideye', 'w3af.sf.net', 'w3af.sourceforge.net', 'w3af.org', 'webbandit', 'webinspect', 'webshag', 'webtrends security analyzer', 'webvulnscan', 'whatweb', 'whcc/', 'wordpress hash grabber', 'xmlrpc exploit', 'WPScan', 'XSpider', 'SF/', 'FooBar/42', 'ScanAlert', 'Webscanner', 'Webster', 'fantomCrew', 'fantomBrowser', 'visvo', 'magereport', 'ltx71', 'websiteprotection', 'BigCliqueBOT', '(BOT for JCE)')
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

OracleWebLogicServerEvent

Stage 2: where

| where isnotempty(HttpUserAgentOriginal)

Stage 3: where

| where HttpUserAgentOriginal has_any ('Nikto', '(hydra)', '.nasl', 'absinthe', 'advanced email extractor', 'arachni/', 'autogetcontent', 'bilbo', 'BFAC', 'brutus', 'brutus/aet', 'bsqlbf', 'cgichk', 'cisco-torch', 'commix', 'core-project/1.0', 'crimscanner/', 'datacha0s', 'dirbuster', 'domino hunter', 'dotdotpwn', 'email extractor', 'fhscan core 1.', 'floodgate', 'get-minimal', 'gootkit auto-rooter scanner', 'grabber', 'grendel-scan', 'havij', 'inspath', 'internet ninja', 'jaascois', 'zmeu', 'masscan', 'metis', 'morfeus', 'mysqloit', 'n-stealth', 'nessus', 'netsparker', 'nmap nse', 'nmap scripting engine', 'nmap-nse', 'nsauditor', 'openvas', 'pangolin', 'paros', 'pmafind', 'prog.customcrawler', 'qualys was', 's.t.a.l.k.e.r.', 'security scan', 'springenwerk', 'sql power injector', 'sqlmap', 'sqlninja', 'teh forest lobster', 'this is an exploit', 'toata dragostea', 'toata dragostea mea pentru diavola', 'uil2pn', 'user-agent:', 'vega/', 'voideye', 'w3af.sf.net', 'w3af.sourceforge.net', 'w3af.org', 'webbandit', 'webinspect', 'webshag', 'webtrends security analyzer', 'webvulnscan', 'whatweb', 'whcc/', 'wordpress hash grabber', 'xmlrpc exploit', 'WPScan', 'XSpider', 'SF/', 'FooBar/42', 'ScanAlert', 'Webscanner', 'Webster', 'fantomCrew', 'fantomBrowser', 'visvo', 'magereport', 'ltx71', 'websiteprotection', 'BigCliqueBOT', '(BOT for JCE)')

Stage 4: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
HttpUserAgentOriginalis_not_null
  • (no value, null check)
field:"HttpUserAgentOriginal" kind:is_not_null
HttpUserAgentOriginalmatch
  • (BOT for JCE) transforms: term
  • (hydra) transforms: term
  • .nasl transforms: term
  • BFAC transforms: term
  • BigCliqueBOT transforms: term
  • FooBar/42 transforms: term
  • Nikto transforms: term
  • SF/ transforms: term
  • ScanAlert transforms: term
  • WPScan transforms: term
  • Webscanner transforms: term
  • Webster transforms: term
  • XSpider transforms: term
  • absinthe transforms: term
  • advanced email extractor transforms: term
  • arachni/ transforms: term
  • autogetcontent transforms: term
  • bilbo transforms: term
  • brutus transforms: term
  • brutus/aet transforms: term
  • bsqlbf transforms: term
  • cgichk transforms: term
  • cisco-torch transforms: term
  • commix transforms: term
  • core-project/1.0 transforms: term
  • crimscanner/ transforms: term
  • datacha0s transforms: term
  • dirbuster transforms: term
  • domino hunter transforms: term
  • dotdotpwn transforms: term
  • email extractor transforms: term
  • fantomBrowser transforms: term
  • fantomCrew transforms: term
  • fhscan core 1. transforms: term
  • floodgate transforms: term
  • get-minimal transforms: term
  • gootkit auto-rooter scanner transforms: term
  • grabber transforms: term
  • grendel-scan transforms: term
  • havij transforms: term
  • +51 more values (see full rule source)
field:"HttpUserAgentOriginal" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Regsvr32 Rundll32 with Anomalous Parent Process

#
Status
available
Severity
high
Time window
1h
Group by
DeviceName, InvestigatedProcessCommandLine, InvestigatedProcessId, InvestigatedProcessName, InvestigatedProcessStartTime
Source
github.com/Azure/Azure-Sentinel

This analytical rule looks for rundll32.exe or regsvr32.exe being spawned by abnormal processes: wscript.exe, powershell.exe, cmd.exe, pwsh.exe, cscript.exe. Blog: https://threathunt.blog/running-live-malware-for-threat-hunting-purposes/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 2624fc55-0998-4897-bb48-1c6422befce4
name: Regsvr32 Rundll32 with Anomalous Parent Process
description: |
  This analytical rule looks for rundll32.exe or regsvr32.exe being spawned by abnormal processes: wscript.exe, powershell.exe, cmd.exe, pwsh.exe, cscript.exe.
  Blog: https://threathunt.blog/running-live-malware-for-threat-hunting-purposes/
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
      - DeviceNetworkEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1218.010
  - T1218.011
tags:
  - Defense Evasion
  - Image Load
  - Regsvr32 Abuse
  - Rundll32 Abuse
query: |
  DeviceProcessEvents
  | where FileName has_any ("rundll32.exe","regsvr32.exe")
  | where InitiatingProcessFileName has_any ("wscript.exe","powershell.exe","cmd.exe","pwsh.exe","cscript.exe")
  | project TimeGenerated, DeviceName, InvestigatedProcessName=FileName, InvestigatedProcessCommandLine = ProcessCommandLine,InvestigatedProcessStartTime = ProcessCreationTime, InvestigatedProcessId = ProcessId, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessParentFileName
  | join (
  DeviceNetworkEvents
  | where InitiatingProcessFileName has_any ("rundll32.exe","regsvr32.exe")
  | where RemoteIPType == "Public"
  | project DeviceName, InvestigatedProcessName=InitiatingProcessFileName, InvestigatedProcessCommandLine = InitiatingProcessCommandLine,InvestigatedProcessStartTime = InitiatingProcessCreationTime, InvestigatedProcessId = InitiatingProcessId, LocalIP, RemoteIP, RemoteUrl
  ) on DeviceName, InvestigatedProcessCommandLine, InvestigatedProcessId, InvestigatedProcessName, InvestigatedProcessStartTime
  | project-away DeviceName1, InvestigatedProcessCommandLine1, InvestigatedProcessId1, InvestigatedProcessName1, InvestigatedProcessStartTime1
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: LocalIP
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: RemoteIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: RemoteUrl
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where FileName has_any ("rundll32.exe","regsvr32.exe")

Stage 3: where

| where InitiatingProcessFileName has_any ("wscript.exe","powershell.exe","cmd.exe","pwsh.exe","cscript.exe")

Stage 4: project

| project TimeGenerated, DeviceName, InvestigatedProcessName=FileName, InvestigatedProcessCommandLine = ProcessCommandLine,InvestigatedProcessStartTime = ProcessCreationTime, InvestigatedProcessId = ProcessId, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessParentFileName

Stage 5: join

| join (
DeviceNetworkEvents
| where InitiatingProcessFileName has_any ("rundll32.exe","regsvr32.exe")
| where RemoteIPType == "Public"
| project DeviceName, InvestigatedProcessName=InitiatingProcessFileName, InvestigatedProcessCommandLine = InitiatingProcessCommandLine,InvestigatedProcessStartTime = InitiatingProcessCreationTime, InvestigatedProcessId = InitiatingProcessId, LocalIP, RemoteIP, RemoteUrl
) on DeviceName, InvestigatedProcessCommandLine, InvestigatedProcessId, InvestigatedProcessName, InvestigatedProcessStartTime

Stage 6: project-away

project-away DeviceName1, InvestigatedProcessCommandLine1, InvestigatedProcessId1, InvestigatedProcessName1, InvestigatedProcessStartTime1

Stage 7: extend

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

Stage 8: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNamematch
  • regsvr32.exe transforms: term corpus 2 (kusto 2)
  • rundll32.exe transforms: term corpus 2 (kusto 2)
field:"process_basename" kind:match
InitiatingProcessFileNamematch
  • cmd.exe transforms: term corpus 2 (kusto 2)
  • cscript.exe transforms: term
  • powershell.exe transforms: term corpus 2 (kusto 2)
  • pwsh.exe transforms: term
  • regsvr32.exe transforms: term
  • rundll32.exe transforms: term
  • wscript.exe transforms: term
field:"parent_process_basename" kind:match
RemoteIPTypeeq
  • Public
field:"RemoteIPType" kind:eq value:"Public"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DeviceNameproject
InitiatingProcessCommandLineproject
InitiatingProcessFileNameproject
InitiatingProcessParentFileNameproject
InvestigatedProcessCommandLineproject
InvestigatedProcessIdproject
InvestigatedProcessNameproject
InvestigatedProcessStartTimeproject
TimeGeneratedproject
HostNameextend
DnsDomainextend

Sentinel One - Exclusion added

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

'Detects when new exclusion added.'

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Rule Body

id: 4224409f-a7bf-45eb-a931-922d79575a05
name: Sentinel One - Exclusion added
description: |
  'Detects when new exclusion added.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SentinelOne
    dataTypes:
      - SentinelOne
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1070
query: | 
  SentinelOne
  | where EventOriginalMessage has_all ('added', 'exclusion')
  | project EventCreationTime, SrcUserName, EventOriginalMessage
  | extend AccountCustomEntity = SrcUserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

SentinelOne

Stage 2: where

| where EventOriginalMessage has_all ('added', 'exclusion')

Stage 3: project

| project EventCreationTime, SrcUserName, EventOriginalMessage

Stage 4: extend

| extend AccountCustomEntity = SrcUserName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventOriginalMessagematch
  • added transforms: term
  • exclusion transforms: term
field:"EventOriginalMessage" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventCreationTimeproject
EventOriginalMessageproject
SrcUserNameproject
AccountCustomEntityextend

Sonrai Ticket Snoozed

#

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

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

'Checks if Sonrai tickets have been snoozed. It uses the action type to check if a ticket has been snoozed'

MITRE ATT&CK Coverage

Rule Body

id: 10e6c454-5cad-4f86-81ce-800235cb050a
name: Sonrai Ticket Snoozed
description: |
  'Checks if Sonrai tickets have been snoozed. 
  It uses the action type to check if a ticket has been snoozed'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SonraiDataConnector
    dataTypes:
      - Sonrai_Tickets_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - CommandAndControl
  - CredentialAccess
  - DefenseEvasion
  - Discovery
  - Execution
  - Exfiltration
  - Impact
  - InitialAccess
  - LateralMovement
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1566
  - T1059
  - T1547
  - T1548
  - T1562
  - T1003
  - T1087
  - T1021
  - T1119
  - T1071
  - T1041
  - T1499
query: |
  Sonrai_Tickets_CL
  | where action_d == 6
entityMappings: 
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: digest_criticalResourceName_s
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  ticketName: digest_title_s
  ticketStatus: digest_status_s
  ticketSeverity: digest_severityCategory_s
  resourceLabel: digest_resourceLabel_s
  resourceType: digest_resourceType_s
  criticalResource: digest_criticalResourceName_s
  ticketOrg: digest_org_s
alertDetailsOverride:
  alertSeverityColumnName: digest_severityCategory_s
  alertDisplayNameFormat: Snoozed - {{digest_ticketSrn_s}} - {{digest_ticketKeyName_s}}
  alertDescriptionFormat: digest_ticketKeyDescription_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

Sonrai_Tickets_CL

Stage 2: where

| where action_d == 6

Indicators

These rows show field, operator, and value matches.

Tailscale: Tailnet lock validation failed

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

Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Suspicious - likely an unsigned node attempting to join.

MITRE ATT&CK Coverage

Rule Body

id: e9f0a1b2-3456-7890-12cd-ef1234560040
name: "Tailscale: Tailnet lock validation failed"
description: Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Suspicious - likely an unsigned node attempting to join.
description-detailed: |
  Identifies tailnet devices with a non-empty TailnetLockError, indicating the device failed tailnet-lock cryptographic validation. Tailnet lock requires new node-keys be co-signed by trusted signers; errors here are the only direct signal of a node-key injection attempt.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Devices_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - InitialAccess
relevantTechniques:
  - T1556
  - T1078
query: |
  Tailscale_Devices_CL
  | where TimeGenerated > ago(1h)
  | summarize arg_max(TimeGenerated, *) by DeviceId
  | where isnotempty(TailnetLockError)
  | project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, TailnetLockError, TailnetLockKey, LastSeen, Authorized
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Devices_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: summarize

| summarize arg_max(TimeGenerated, *) by DeviceId

Stage 4: where

| where isnotempty(TailnetLockError)

Stage 5: project

| project TimeGenerated, DeviceName, Hostname, User, Os, ClientVersion, TailnetLockError, TailnetLockKey, LastSeen, Authorized

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
TailnetLockErroris_not_null
  • (no value, null check)
field:"TailnetLockError" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Authorizedproject
ClientVersionproject
DeviceNameproject
Hostnameproject
LastSeenproject
Osproject
TailnetLockErrorproject
TailnetLockKeyproject
TimeGeneratedproject
Userproject

Ubiquiti - Unusual traffic

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

'Detects unusual traffic masking as HTTP(S).'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 31e868c0-91d3-40eb-accc-3fa73aa96f8e
name: Ubiquiti - Unusual traffic
description: |
  'Detects unusual traffic masking as HTTP(S).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - Ubiquiti_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1573
query: |
  UbiquitiAuditEvent
  | where EventCategory =~ 'firewall'
  | where DstPortNumber in ('80', '443')
  | where NetworkProtocol !~ 'TCP'
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

UbiquitiAuditEvent

Stage 2: where

| where EventCategory =~ 'firewall'

Stage 3: where

| where DstPortNumber in ('80', '443')

Stage 4: where

| where NetworkProtocol !~ 'TCP'

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
IPCustomEntityextend

Vaikora - Agent policy violation

#

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
Group by
AgentId
Source
github.com/Azure/Azure-Sentinel

Identifies AI agent actions explicitly blocked by a Vaikora policy. Repeated violations from the same agent may indicate prompt injection, policy circumvention, or a compromised workflow.

MITRE ATT&CK Coverage

TacticTechniques
Stealth
Defense Impairment

Rule Body

id: 54262ad1-f346-4246-a13f-9557595ff7bd
name: Vaikora - Agent policy violation
description: |
  Identifies AI agent actions explicitly blocked by a Vaikora policy. Repeated violations from the same agent may indicate prompt injection, policy circumvention, or a compromised workflow.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: VaikoraSentinel
    dataTypes:
      - Vaikora_AgentSignals_CL
queryFrequency: 15m
queryPeriod: 1h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
  - Impact
  - DefenseEvasion
relevantTechniques:
  - T1078
  - T1562
query: |
  Vaikora_AgentSignals_CL
  | where TimeGenerated > ago(1h)
  | where policy_decision_s == "block"
  | summarize
      ViolationCount = count(),
      PolicyIds = make_set(policy_id_s),
      ActionTypes = make_set(action_type_s),
      ResourceTypes = make_set(resource_type_s),
      MaxAnomalyScore = max(anomaly_score_d),
      Severities = make_set(severity_s),
      LogHashes = make_set(log_hash_s)
    by AgentId = agent_id_s
  | extend
      PolicyList = strcat_array(PolicyIds, ", "),
      ActionList = strcat_array(ActionTypes, ", "),
      ResourceList = strcat_array(ResourceTypes, ", ")
  | where ViolationCount >= 1
suppressionDuration: 15m
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: Selected
    groupByEntities:
      - Account
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  ViolationCount: ViolationCount
  PolicyIds: PolicyList
  ActionTypes: ActionList
  MaxAnomalyScore: MaxAnomalyScore
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AgentId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Vaikora_AgentSignals_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where policy_decision_s == "block"

Stage 4: summarize

| summarize
    ViolationCount = count(),
    PolicyIds = make_set(policy_id_s),
    ActionTypes = make_set(action_type_s),
    ResourceTypes = make_set(resource_type_s),
    MaxAnomalyScore = max(anomaly_score_d),
    Severities = make_set(severity_s),
    LogHashes = make_set(log_hash_s)
  by AgentId = agent_id_s

Stage 5: extend

| extend
    PolicyList = strcat_array(PolicyIds, ", "),
    ActionList = strcat_array(ActionTypes, ", "),
    ResourceList = strcat_array(ResourceTypes, ", ")

Stage 6: where

| where ViolationCount >= 1

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionTypessummarize
AgentIdsummarize
LogHashessummarize
MaxAnomalyScoresummarize
PolicyIdssummarize
ResourceTypessummarize
Severitiessummarize
ViolationCountsummarize
ActionListextend
PolicyListextend
ResourceListextend

Whisper Security - Domain Registrar Change Anomaly

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

Detects domains whose registrar has changed across WHOIS history snapshots within the last 30 days. Registrar changes may indicate domain hijacking or adversary infrastructure acquisition.

MITRE ATT&CK Coverage

TacticTechniques
Resource Development

Rule Body

id: b6e26c67-f596-5c0f-8614-c88d715508c5
name: Whisper Security - Domain Registrar Change Anomaly
description: |
  Detects domains whose registrar has changed across WHOIS history snapshots within the last 30 days. Registrar changes may indicate domain hijacking or adversary infrastructure acquisition.
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: WhisperSecurityConnector
  dataTypes:
  - WhisperHistory_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- ResourceDevelopment
relevantTechniques:
- T1584
query: |
  // MITRE ATT&CK: T1584 - Compromise Infrastructure
  // Tactic: Resource Development
  // Detects registrar changes across WHOIS history snapshots
  let lookbackPeriod = 14d;
  WhisperHistory_CL
      | where TimeGenerated > ago(lookbackPeriod)
      | where indicatorType == "domain"
      | where isnotempty(registrar)
      | summarize Registrars = make_set(registrar), SnapshotCount = count(), EarliestSnapshot = min(snapshotDate), LatestSnapshot = max(snapshotDate) by indicator
      | where array_length(Registrars) > 1
      | mv-expand CurrentRegistrar = Registrars
      | extend CurrentRegistrar = tostring(CurrentRegistrar)
      | join kind=inner (
          WhisperHistory_CL
          | where TimeGenerated > ago(lookbackPeriod)
          | where indicatorType == "domain"
          | where isnotempty(registrar)
          | serialize
          | extend PreviousRegistrar = prev(registrar), PreviousIndicator = prev(indicator)
          | where indicator == PreviousIndicator
          | where registrar != PreviousRegistrar
          | where isnotempty(PreviousRegistrar)
          | project indicator, OldRegistrar = PreviousRegistrar, NewRegistrar = registrar, ChangeDate = snapshotDate
      ) on indicator
      | project TimeGenerated = ChangeDate, DnsDomain = indicator, OldRegistrar, NewRegistrar, ChangeDate, SnapshotCount
      | distinct TimeGenerated, DnsDomain, OldRegistrar, NewRegistrar, ChangeDate, SnapshotCount
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: DnsDomain
customDetails:
  OldRegistrar: OldRegistrar
  NewRegistrar: NewRegistrar
  ChangeDate: ChangeDate
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookbackPeriod = 14d;

Stage 1: source

WhisperHistory_CL

Stage 2: where

| where TimeGenerated > ago(lookbackPeriod)

Stage 3: where

| where indicatorType == "domain"

Stage 4: where

| where isnotempty(registrar)

Stage 5: summarize

| summarize Registrars = make_set(registrar), SnapshotCount = count(), EarliestSnapshot = min(snapshotDate), LatestSnapshot = max(snapshotDate) by indicator

Stage 6: where

| where array_length(Registrars) > 1

Stage 7: mv-expand

| mv-expand CurrentRegistrar = Registrars

Stage 8: extend

| extend CurrentRegistrar = tostring(CurrentRegistrar)

Stage 9: join

| join kind=inner (
        WhisperHistory_CL
        | where TimeGenerated > ago(lookbackPeriod)
        | where indicatorType == "domain"
        | where isnotempty(registrar)
        | serialize
        | extend PreviousRegistrar = prev(registrar), PreviousIndicator = prev(indicator)
        | where indicator == PreviousIndicator
        | where registrar != PreviousRegistrar
        | where isnotempty(PreviousRegistrar)
        | project indicator, OldRegistrar = PreviousRegistrar, NewRegistrar = registrar, ChangeDate = snapshotDate
    ) on indicator

Stage 10: project

project ChangeDate, DnsDomain, NewRegistrar, OldRegistrar, SnapshotCount, TimeGenerated

Stage 11: distinct

distinct ChangeDate, DnsDomain, NewRegistrar, OldRegistrar, SnapshotCount, TimeGenerated

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ChangeDateproject
DnsDomainproject
NewRegistrarproject
OldRegistrarproject
SnapshotCountproject
TimeGeneratedproject

Whisper Security - SPF Record Unauthorized Include Detection

#
Status
available
Severity
high
Time window
1d
Group by
SpfInclude, indicator
Source
github.com/Azure/Azure-Sentinel

Detects new SPF include directives on monitored domains that were not seen in the previous scan window. Unauthorized SPF modifications may allow adversaries to send phishing emails that pass SPF checks.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 9c275139-b554-58f1-b390-8520b10e54ad
name: Whisper Security - SPF Record Unauthorized Include Detection
description: |
  Detects new SPF include directives on monitored domains that were not seen in the previous scan window. Unauthorized SPF modifications may allow adversaries to send phishing emails that pass SPF checks.
severity: High
status: Available
requiredDataConnectors:
- connectorId: WhisperSecurityConnector
  dataTypes:
  - WhisperInfraContext_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
relevantTechniques:
- T1566
query: |
  // MITRE ATT&CK: T1566.001 - Phishing: Spearphishing Attachment
  // Tactic: Initial Access
  // Detects unauthorized SPF include directives not seen in previous scans
  // Configure authorized_spf_includes with your organization's known SPF providers
  let authorized_spf_includes = dynamic(["_spf.google.com", "spf.protection.outlook.com", "amazonses.com", "sendgrid.net", "mailgun.org"]);
  let previousIncludes = WhisperInfraContext_CL
      | where TimeGenerated between (ago(1d) .. ago(1h))
      | where isnotempty(spfIncludes)
      | mv-expand SpfInclude = split(spfIncludes, ",")
      | extend SpfInclude = tostring(SpfInclude)
      | summarize PreviousIncludes = make_set(SpfInclude) by indicator;
  let currentIncludes = WhisperInfraContext_CL
      | where TimeGenerated > ago(1h)
      | where isnotempty(spfIncludes)
      | mv-expand SpfInclude = split(spfIncludes, ",")
      | extend SpfInclude = tostring(SpfInclude)
      | project indicator, SpfInclude, TimeGenerated;
  currentIncludes
      | join kind=leftanti (previousIncludes | mv-expand PrevInclude = PreviousIncludes | extend SpfInclude = tostring(PrevInclude) | project indicator, SpfInclude) on indicator, SpfInclude
      | where SpfInclude !in (authorized_spf_includes)
      | project TimeGenerated, DnsDomain = indicator, UnauthorizedInclude = SpfInclude
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: DnsDomain
customDetails:
  Domain: DnsDomain
  UnauthorizedInclude: UnauthorizedInclude
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let authorized_spf_includes = dynamic(["_spf.google.com", "spf.protection.outlook.com", "amazonses.com", "sendgrid.net", "mailgun.org"]);

let currentIncludes is inlined into the numbered stages below.

Let Binding: previousIncludes used in Stage 7

let previousIncludes = WhisperInfraContext_CL
    | where TimeGenerated between (ago(1d) .. ago(1h))
    | where isnotempty(spfIncludes)
    | mv-expand SpfInclude = split(spfIncludes, ",")
    | extend SpfInclude = tostring(SpfInclude)
    | summarize PreviousIncludes = make_set(SpfInclude) by indicator;

Stages 1 to 6 define let currentIncludes as the rule's main pipeline source. Stages 7 to 10 use it.

Stage 1: source

WhisperInfraContext_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where isnotempty(spfIncludes)

Stage 4: mv-expand

| mv-expand SpfInclude = split(spfIncludes, ",")

Stage 5: extend

| extend SpfInclude = tostring(SpfInclude)

Stage 6: project

| project indicator, SpfInclude, TimeGenerated

Stage 7: join (negated)

join kind=leftanti (previousIncludes) on indicator, SpfInclude

Stage 8: where

where not (SpfInclude in ("_spf.google.com", "amazonses.com", "mailgun.org", "sendgrid.net", "spf.protection.outlook.com"))

Stage 9: project

project DnsDomain, TimeGenerated, UnauthorizedInclude

Stage 10: summarize aggregation inside the join branch

summarize by indicator

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
spfIncludesis_not_null
  • (no value, null check)
field:"spfIncludes" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
spfIncludesis_not_null(no value, null check)excludes:spfIncludes
SpfIncludein_spf.google.com, amazonses.com, mailgun.org, sendgrid.net, spf.protection.outlook.comexcludes:SpfInclude

Output Fields

These fields are emitted when the rule matches.

FieldSource
indicatorsummarize

Back to all Kusto rules