Detection rules › Kusto

Kusto rules: 3f

AWSCloudTrail - Creation of new CRUD IAM policy and then privilege escalation

#
Status
available
Severity
medium
Time window
1d
Group by
AccountName, AccountUPNSuffix, EventName, EventSource, PolicyName, RecipientAccountId, SourceIpAddress, UserIdentityArn, UserIdentityType
Source
github.com/Azure/Azure-Sentinel

Identifies creation of IAM policies that grant broad IAM CRUD permissions, followed by attachment of that policy to a user, role, or group. This pattern may indicate an attempt to establish or expand privileged access and should be reviewed as potential privilege escalation.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 8a607285-d95c-473d-8aab-59920de63af6
name: AWSCloudTrail - Creation of new CRUD IAM policy and then privilege escalation
description: |
  Identifies creation of IAM policies that grant broad IAM CRUD permissions, followed by attachment of that policy to a user, role, or group. This pattern may indicate an attempt to establish or expand
  privileged access and should be reviewed as potential privilege escalation.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1484
  - T1098.003
query: |
  let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
  let createPolicy =  dynamic(["CreatePolicy", "CreatePolicyVersion"]);
  let timeframe = 1d;
  let lookback = 14d;
  // Creating Master table with all the events to use with materialize for better performance
  let EventInfo = AWSCloudTrail
  | where TimeGenerated >= ago(lookback)
  | where EventName in (EventNameList) or EventName in (createPolicy)
    | 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]), "");
  //Checking for Policy creation event with Full Admin Privileges since lookback period.
  let FullAdminPolicyEvents =  materialize(  EventInfo
  | where TimeGenerated >= ago(lookback)
  | where EventName in (createPolicy)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and (Action contains "iam:Create" and (Action contains "iam:Get" or Action contains "iam:List")  and Action contains "iam:Update" and Action contains "iam:Delete") and Resource == "*" and Condition == ""
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn
  | project-rename StartTime = TimeGenerated  );
  let PolicyAttach = materialize(  EventInfo
  | where TimeGenerated >= ago(timeframe)
  | where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
  | summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityType , UserIdentityArn, SourceIpAddress, PolicyName
  | extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType, "SourceIpAddress", SourceIpAddress,"AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
  | project EventSource, PolicyName, AttachEvent, AttachEventCount, RecipientAccountId, AccountName, AccountUPNSuffix
  );
  // Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
  // These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
  FullAdminPolicyEvents
  | join kind=leftouter
  (
      PolicyAttach
  )
  on PolicyName
  | project-away PolicyName1
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  RecipientAccountId: RecipientAccountId
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS CRUD IAM privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
let createPolicy = dynamic(["CreatePolicy", "CreatePolicyVersion"]);
let timeframe = 1d;
let lookback = 14d;

let EventInfo and let FullAdminPolicyEvents are inlined into the numbered stages below.

Let Binding: PolicyAttach used in Stage 15

let PolicyAttach = materialize(  EventInfo
| where TimeGenerated >= ago(timeframe)
| where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
| extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
| summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName,   RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityType , UserIdentityArn, SourceIpAddress, PolicyName
| extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType, "SourceIpAddress", SourceIpAddress,"AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
| project EventSource, PolicyName, AttachEvent, AttachEventCount, RecipientAccountId, AccountName, AccountUPNSuffix
);

Stages 1 to 14 define let FullAdminPolicyEvents as the rule's main pipeline source. Stages 15 to 17 use it.

Stage 1: source

AWSCloudTrail

Stage 2: where

| where TimeGenerated >= ago(lookback)

Stage 3: where

| where EventName in (EventNameList) or EventName in (createPolicy)

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]), "")

Stage 5: where

| where TimeGenerated >= ago(lookback)

Stage 6: where

| where EventName in (createPolicy)

Stage 7: extend

| extend PolicyName = tostring(parse_json(RequestParameters).policyName)

Stage 8: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement

Stage 9: mv-expand

| mvexpand Statement

Stage 10: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)

Stage 11: extend

| extend Action = tostring(Action)

Stage 12: where

| where Effect =~ "Allow" and (Action contains "iam:Create" and (Action contains "iam:Get" or Action contains "iam:List")  and Action contains "iam:Update" and Action contains "iam:Delete") and Resource == "*" and Condition == ""

Stage 13: distinct

| distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn

Stage 14: project-rename

| project-rename StartTime = TimeGenerated

Stage 15: join

FullAdminPolicyEvents
| join kind=leftouter
(
    PolicyAttach
)
on PolicyName

Stage 16: project-away

project-away PolicyName1

Stage 17: summarize aggregation inside the join branch

summarize by EventSource, EventName, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityType, UserIdentityArn, SourceIpAddress, PolicyName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • iam:Create
  • iam:Delete
  • iam:Get
  • iam:List
  • iam:Update
field:"Action" kind:contains
Effecteq
  • Allow
field:"Effect" kind:eq value:"Allow"
ErrorCodeis_null
  • (no value, null check)
field:"ErrorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"ErrorMessage" kind:is_null
EventNamein
  • AttachGroupPolicy corpus 17 (kusto 15, panther 2)
  • AttachGroupPolicy
  • AttachRolePolicy corpus 17 (kusto 15, panther 2)
  • AttachRolePolicy
  • AttachUserPolicy corpus 17 (kusto 15, panther 2)
  • AttachUserPolicy
  • CreatePolicy corpus 14 (kusto 12, panther 2)
  • CreatePolicy
  • CreatePolicyVersion corpus 14 (kusto 12, panther 2)
  • CreatePolicyVersion
field:"aws::eventName" kind:in
Resourceeq
  • *
field:"Resource" kind:eq value:"*"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNamesummarize
AccountUPNSuffixsummarize
EventNamesummarize
EventSourcesummarize
PolicyNamesummarize
RecipientAccountIdsummarize
SourceIpAddresssummarize
UserIdentityArnsummarize
UserIdentityTypesummarize

AWSCloudTrail - S3 bucket access point publicly exposed

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

Detects S3 access point policy changes that allow public principals. Public exposure through access point configuration can lead to unauthorized data access and potential information disclosure.

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Telemetry Coverage

Rule Body

id: b7a44e0d-ae4c-4fb2-be1b-aa0e45f2327b
name: AWSCloudTrail - S3 bucket access point publicly exposed
description: |
  Detects S3 access point policy changes that allow public principals. Public exposure through access point
  configuration can lead to unauthorized data access and potential information disclosure.
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 == "PutAccessPointPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).PutAccessPointPolicyRequest.Policy))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition), Principal_aws = tostring(parse_json(Statement).Principal.AWS), Principal = tostring(parse_json(Statement).Principal)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and (Principal_aws == "*" or Principal == "*") and isempty(Condition)
  | 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]), "")
  | distinct TimeGenerated, EventName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName
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
  UserIdentityArn: UserIdentityArn
  RecipientAccountId: RecipientAccountId
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS S3 access point publicly exposed by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} from {{SourceIpAddress}} creating public S3 access point exposure in account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName == "PutAccessPointPolicy" and isempty(ErrorCode) and isempty(ErrorMessage)

Stage 3: extend

| extend Statement = parse_json(tostring((parse_json(RequestParameters).PutAccessPointPolicyRequest.Policy))).Statement

Stage 4: mv-expand

| mvexpand Statement

Stage 5: extend

| extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition), Principal_aws = tostring(parse_json(Statement).Principal.AWS), Principal = tostring(parse_json(Statement).Principal)

Stage 6: extend

| extend Action = tostring(Action)

Stage 7: where

| where Effect =~ "Allow" and (Principal_aws == "*" or Principal == "*") and isempty(Condition)

Stage 8: extend (4 consecutive steps)

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

Stage 9: distinct

| distinct TimeGenerated, EventName, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityArn, UserIdentityUserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Statementextend
Actionextend
Conditionextend
Effectextend
Principalextend
Principal_awsextend
Resourceextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend

BloodHound Attack Path Finding - Legacy SID History on Tier Zero Objects

#

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

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

BloodHound Attack Path Finding - Legacy SID History on Tier Zero Objects

Rule Body

id: 92914273-eb38-4a54-9c5d-03db8d24fba7
name: BloodHound Attack Path Finding - Legacy SID History on Tier Zero Objects
description: BloodHound Attack Path Finding - Legacy SID History on Tier Zero Objects
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0HasSIDHistory"
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 == "T0HasSIDHistory"

Indicators

These rows show field, operator, and value matches.

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

Conditional Access - A Conditional Access policy was deleted

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

A Conditional Access policy was deleted from Entra ID.

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
AzureDelete conditional access policy

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2e96fa64-ac4d-4c92-b79e-e9c54b5d8230
name: Conditional Access - A Conditional Access policy was deleted
version: 1.0.1
kind: Scheduled
description: A Conditional Access policy was deleted from Entra ID.
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
  - T1562.007
query: |+
  // A Conditional Access policy was deleted.
  AuditLogs
  | where OperationName in ("Delete conditional access policy")
  | extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
  | extend accountName = tostring(split(modifiedBy, "@")[0])
  | extend upnSuffix = tostring(split(modifiedBy, "@")[1])
  | project
      TimeGenerated,
      OperationName,
      policy = TargetResources[0].displayName,
      modifiedBy,
      accountName,
      upnSuffix,
      result = Result,
      oldPolicy = TargetResources[0].modifiedProperties[0].oldValue
  | order by TimeGenerated desc
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: accountName
  - identifier: UPNSuffix
    columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName in ("Delete conditional access policy")

Stage 3: extend (3 consecutive steps)

| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])

Stage 4: project

| project
    TimeGenerated,
    OperationName,
    policy = TargetResources[0].displayName,
    modifiedBy,
    accountName,
    upnSuffix,
    result = Result,
    oldPolicy = TargetResources[0].modifiedProperties[0].oldValue

Stage 5: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
OperationNameproject
TimeGeneratedproject
accountNameproject
modifiedByproject
oldPolicyproject
policyproject
resultproject
upnSuffixproject

Copilot - Jailbreak Attempt Detected

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

'Detects jailbreak attempts in Copilot interactions where users are trying to bypass Copilot guardrails and security controls. This rule identifies prompt injection and LLM abuse scenarios that could lead to initial access, credential access, or system impact.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access
Impact

Rule Body

id: e5f6a7b8-c9d0-41e2-f3a4-b5c6d7e8f9a0
name: Copilot - Jailbreak Attempt Detected
description: |
  'Detects jailbreak attempts in Copilot interactions where users are trying to bypass Copilot guardrails and security controls.
  This rule identifies prompt injection and LLM abuse scenarios that could lead to initial access, credential access, or system impact.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftCopilot
    dataTypes:
      - CopilotActivity
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
  - Impact
relevantTechniques:
  - T1078
  - T1110
  - T1565
query: |
  CopilotActivity
  | where RecordType == "CopilotInteraction"
  | where LLMEventData has "JailbreakDetected"
  | extend Data = parse_json(LLMEventData)
  | extend Jailbreak = tostring(Data.Messages[0].JailbreakDetected)
  | where Jailbreak == "true"
  | project TimeGenerated, ActorName, AIModelName, Jailbreak
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CopilotActivity

Stage 2: where

| where RecordType == "CopilotInteraction"

Stage 3: where

| where LLMEventData has "JailbreakDetected"

Stage 4: extend

| extend Data = parse_json(LLMEventData)

Stage 5: extend

| extend Jailbreak = tostring(Data.Messages[0].JailbreakDetected)

Stage 6: where

| where Jailbreak == "true"

Stage 7: project

| project TimeGenerated, ActorName, AIModelName, Jailbreak

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AIModelNameproject
ActorNameproject
Jailbreakproject
TimeGeneratedproject

Corelight - Possible Webshell (Rare PUT or POST)

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

'Detects rare post requests to a single webserver location.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: db662e49-6e34-4d10-9d3c-5d04b5479658
name: Corelight - Possible Webshell (Rare PUT or POST)
description: |
  'Detects rare post requests to a single webserver location.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Corelight
    dataTypes:
      - corelight_http
      - corelight_http_red
      - corelight_http2
      - corelight_http_agg
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1505
query: |
  let threshold = 3;
  union isfuzzy=true corelight_http, corelight_http_agg
  | where method in~ ('POST', 'PUT')
  | where toint(status_code) !between (400 .. 499)
  | where request_body_len != 0 or response_body_len != 0
  | extend fe = extract(@'.*(\.\w+)$', 1, uri)
  | where fe in~ ('.aspx', '.asp', '.php', '.jsp', '.jspx', '.war', '.ashx', '.asmx', '.ascx', '.asx', '.cshtml', '.cfm', '.cfc', '.cfml', '.wss', '.do', '.action', '.pl', '.plx', '.pm', '.xs', '.t', '.pod', '.php-s', '.pht', '.phar', '.phps', '.php7', '.php5', '.php4', '.php3', '.phtml', '.py', '.rb', '.rhtml', '.cgi', '.dll', '.ayws', '.cgi', '.erb', '.rjs', '.hta', '.htc', '.cs', '.kt', '.lua', '.vbhtml')
  | summarize count() by uri, id_orig_h
  | where count_ < threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: id_orig_h
version: 2.2.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 3;

Union isfuzzy=true (2 Sources)

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

Leg 1: corelight_http

Leg 2: corelight_http_agg

Applied to the Combined Result

| where method in~ ('POST', 'PUT') | where toint(status_code) !between (400 .. 499) | where request_body_len != 0 or response_body_len != 0 | extend fe = extract(@'.*(\.\w+)$', 1, uri) | where fe in~ ('.aspx', '.asp', '.php', '.jsp', '.jspx', '.war', '.ashx', '.asmx', '.ascx', '.asx', '.cshtml', '.cfm', '.cfc', '.cfml', '.wss', '.do', '.action', '.pl', '.plx', '.pm', '.xs', '.t', '.pod', '.php-s', '.pht', '.phar', '.phps', '.php7', '.php5', '.php4', '.php3', '.phtml', '.py', '.rb', '.rhtml', '.cgi', '.dll', '.ayws', '.cgi', '.erb', '.rjs', '.hta', '.htc', '.cs', '.kt', '.lua', '.vbhtml') | summarize count() by uri, id_orig_h | where count_ < threshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
count_lt
  • 3 transforms: number
field:"count_" kind:lt value:"3"
fein
  • .action
  • .ascx
  • .ashx
  • .asmx
  • .asp
  • .aspx
  • .asx
  • .ayws
  • .cfc
  • .cfm
  • .cfml
  • .cgi
  • .cs
  • .cshtml
  • .dll
  • .do
  • .erb
  • .hta
  • .htc
  • .jsp
  • .jspx
  • .kt
  • .lua
  • .phar
  • .php
  • .php-s
  • .php3
  • .php4
  • .php5
  • .php7
  • .phps
  • .pht
  • .phtml
  • .pl
  • .plx
  • .pm
  • .pod
  • .py
  • .rb
  • .rhtml
  • .rjs
  • .t
  • .vbhtml
  • .war
  • .wss
  • .xs
field:"fe" kind:in
methodin
  • POST
  • PUT
field:"method" kind:in
request_body_lenne
  • 0
field:"request_body_len" kind:ne value:"0"
response_body_lenne
  • 0
field:"response_body_len" kind:ne value:"0"

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
id_orig_hsummarize
urisummarize

Cyble Vision Alerts SSL Certificate Expiry

#

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

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

'Generates Microsoft Sentinel incidents for SSL certificates nearing expiry as detected by Cyble. These alerts help identify certificate hygiene risks that may lead to service disruption or security issues. Severity is normalized using MappedSeverity for downstream automation.'

MITRE ATT&CK Coverage

Rule Body

id: A667D635-D2A7-47E7-8827-8FB243AF2AFD
name: Cyble Vision Alerts SSL Certificate Expiry
description: |
  'Generates Microsoft Sentinel incidents for SSL certificates nearing expiry as detected by Cyble. These alerts help identify certificate hygiene risks that may lead to service disruption or security issues. Severity is normalized using MappedSeverity for downstream automation.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
query: |
  Alerts_ssl_expiry
  | where Service == "ssl_expiry" 
  | extend MappedSeverity = Severity
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- InitialAccess
- Impact
relevantTechniques:
- T1190
- T1499
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: SS_Asset
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: SS_Asset
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: SS_Port
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: SSL Certificate Expiry  {{DC_ServerName}}
  alertDescriptionFormat: ""
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  Asset: SS_Asset
  ExpiryDate: SS_Expiry
  DaysToExpiry: SS_DaysUntilExpiry
  CertificateTitle: SS_Title
  Port: SS_Port
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_ssl_expiry

Stage 2: where

| where Service == "ssl_expiry"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

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

#

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

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

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

MITRE ATT&CK Coverage

Rule Body

id: c919c911-8b01-44f8-9c3b-60b1edfc417f
name: CYFIRMA - High severity File Hash Indicators with Monitor Action and Malware
description: |
  "This KQL query retrieves file hash indicators (MD5, SHA1, SHA256) from the CyfirmaIndicators_CL table within the last 5 minutes. 
  It filters records with a confidence score of 80 or higher, containing file hash patterns, a recommended action of 'Monitor', and roles marked as 'Malware'. 
  Extracted hashes and key threat intelligence details are projected for monitoring and investigation."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // File Hash Indicators with Monitor Action and Malware
  let timeFrame = 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern contains 'file:hashes' and RecommendedActions has 'Monitor' and (Roles contains 'Malware')
  | extend MD5 = extract(@"file:hashes\.md5\s*=\s*'([a-fA-F0-9]{32})'", 1, pattern)
  | extend SHA1 = extract(@"file:hashes\.'SHA-1'\s*=\s*'([a-fA-F0-9]{40})'", 1, pattern)
  | extend SHA256 = extract(@"file:hashes\.'SHA-256'\s*=\s*'([a-fA-F0-9]{64})'", 1, pattern)
  | extend
      Algo_MD5='md5',
      Algo_SHA1= 'SHA1',
      Algo_SHA256='SHA256',
      ProviderName = 'CYFIRMA',
      ProductName = 'DeCYFIR/DeTCT'
  | project  
      MD5,
      Algo_MD5,
      SHA1,
      Algo_SHA1,
      SHA256,
      Algo_SHA256,
      ThreatActors,
      Sources,
      RecommendedActions,
      Roles,
      Country,
      name,
      Description,
      ConfidenceScore,
      SecurityVendors,
      IndicatorID,
      created,
      modified,
      valid_from,
      Tags,
      ThreatType,
      TimeGenerated,
      ProductName,
      ProviderName
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: GreaterThan
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - DefenseEvasion
  - InitialAccess
  - Impact
  - Execution
relevantTechniques:
  - T1027
  - T1486
  - T1204
  - T1485
  - T1218
  - T1566.001
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence File Hash Indicators with Monitor Action and Malware - {{name}} "
  alertDescriptionFormat: "{{Description}} - {{name}} "
  alertDynamicProperties:
    - alertProperty: ProviderName
      value: ProviderName
    - alertProperty: ProductName
      value: ProductName
customDetails:
  ThreatActors: ThreatActors
  Sources: Sources
  RecommendedActions: RecommendedActions
  Roles: Roles
  Country: Country
  Description: Description
  ConfidenceScore: ConfidenceScore
  SecurityVendors: SecurityVendors
  IndicatorID: IndicatorID
  created: created
  modified: modified
  valid_from: valid_from
  Tags: Tags
  ThreatType: ThreatType
  TimeGenerated: TimeGenerated
entityMappings:
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_MD5
      - identifier: Value
        columnName: MD5
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA1
      - identifier: Value
        columnName: SHA1
  - entityType: FileHash
    fieldMappings:
      - identifier: Algorithm
        columnName: Algo_SHA256
      - identifier: Value
        columnName: SHA256
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaIndicators_CL

Stage 2: where

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

Stage 3: extend (4 consecutive steps)

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

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

Dataverse - Malware found in SharePoint document management site

#
Status
available
Severity
medium
Time window
14d
Group by
$left.Site_Url, $right.SharePointUrl, MalwareUserId, Site_Url, SourceFileName, UserId
Source
github.com/Azure/Azure-Sentinel

This query identifies malware uploaded via Dynamics 365 document management or directly in SharePoint impacting Dataverse associated SharePoint sites.

MITRE ATT&CK Coverage

TacticTechniques
Execution

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 2e3878bb-d519-43aa-9992-ea069df099e4
kind: Scheduled
name: Dataverse - Malware found in SharePoint document management site
description: This query identifies malware uploaded via Dynamics 365 document management
  or directly in SharePoint impacting Dataverse associated SharePoint sites.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
  - connectorId: Office365
    dataTypes:
      - OfficeActivity (SharePoint)
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1204
query: |
  let query_frequency = 15m;
    let malware_events = OfficeActivity
        | where OfficeWorkload == "SharePoint" and Operation == "FileMalwareDetected"
        | summarize by MalwareUserId = UserId, SourceFileName, Site_Url
        | join kind=inner (DataverseSharePointSites) on $left.Site_Url == $right.SharePointUrl;
    let file_upload_events = OfficeActivity
        | where OfficeWorkload == "SharePoint" and Operation == "FileUploaded"
        | project TimeGenerated, UserId, Site_Url, SourceFileName, ApplicationId, ClientIP;
    let d365_upload_events = DataverseActivity
        | where TimeGenerated >= ago(query_frequency)
        | where Message == "UploadDocument"
        | summarize by UserId, D365ClientIp = ClientIp;
    malware_events
    | join kind=inner (file_upload_events) on SourceFileName, Site_Url
    | lookup (d365_upload_events) on UserId
    | extend ClientIp = iif(ApplicationId == "00000007-0000-0000-c000-000000000000", D365ClientIp, ClientIP)
    | extend
        CloudAppId = int(32780),
        SharePointId = int(20892),
        AccountName = tostring(split(UserId, '@')[0]),
        UPNSuffix = tostring(split(UserId, '@')[1])
    | project
        TimeGenerated,
        UserId,
        ClientIp,
        InstanceUrl,
        SharePointUrl,
        SourceFileName,
        CloudAppId,
        SharePointId,
        AccountName,
        UPNSuffix
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: SourceFileName
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: SharePointId
      - identifier: InstanceName
        columnName: SharePointUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Malware was found in SharePoint document management
    site for {{InstanceUrl}} '
  alertDescriptionFormat: A malicious file {{SourceFileName}} was found in SharePoint
    site {{SharePointUrl}}. The file was uploaded by {{UserId}}
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 15m;

let malware_events is inlined into the numbered stages below.

Let Binding: file_upload_events used in Stage 5

let file_upload_events = OfficeActivity
      | where OfficeWorkload == "SharePoint" and Operation == "FileUploaded"
      | project TimeGenerated, UserId, Site_Url, SourceFileName, ApplicationId, ClientIP;

Let Binding: d365_upload_events used in Stage 6

let d365_upload_events = DataverseActivity
      | where TimeGenerated >= ago(query_frequency)
      | where Message == "UploadDocument"
      | summarize by UserId, D365ClientIp = ClientIp;

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

Stage 1: source

OfficeActivity

Stage 2: where

where OfficeWorkload == "SharePoint" and Operation == "FileMalwareDetected"

Stage 3: summarize

summarize MalwareUserId by MalwareUserId, SourceFileName, Site_Url

Stage 4: join

| join kind=inner (DataverseSharePointSites) on $left.Site_Url == $right.SharePointUrl

Stage 5: join

malware_events
| join kind=inner (file_upload_events) on SourceFileName, Site_Url

Stage 6: kusto:lookup

| lookup (d365_upload_events) on UserId

Stage 7: extend

extend ClientIp

Stage 8: extend

extend AccountName, CloudAppId, SharePointId, UPNSuffix

Stage 9: project

project AccountName, ClientIp, CloudAppId, InstanceUrl, SharePointId, SharePointUrl, SourceFileName, TimeGenerated, UPNSuffix, UserId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
InstanceUrlproject
SharePointIdproject
SharePointUrlproject
SourceFileNameproject
TimeGeneratedproject
UPNSuffixproject
UserIdproject

Detect Unsigned executable launch from scheduled task

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

Persistence via Scheduled Tasks is a well-known technique used by adversaries to make sure their malware programs keep running an the compromised device. With this detection rule, you can search for unknown executables being launched from scheduled tasks. > [!WARNING] > This detection rule is the base for the detection. You will need to add environment specific finetuning in order to limit the BP detections on legitimate processes

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let scheduled_binaries = (
    DeviceProcessEvents
    | where ActionType !contains "aggregated"
    | where Timestamp > ago(1h)
    | where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
    | distinct SHA1
);
let untrusted_binaries = (
    scheduled_binaries
    | join kind=leftanti (
        DeviceFileCertificateInfo 
        | where Timestamp > ago(1h) 
        | summarize max_trusted=max(IsTrusted) by SHA1 
        | where max_trusted==1
    ) on SHA1
);
untrusted_binaries
| invoke FileProfile(SHA1,1000)
| where IsCertificateValid != 1 // Exclude signed binaries
| where (isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000)
| join (
    DeviceProcessEvents 
    | where ActionType !contains "aggregated"
    | where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"
) on SHA1

Stages and Predicates

let scheduled_binaries and let untrusted_binaries are inlined into the numbered stages below.

Stage 1: source

let scheduled_binaries

Stage 2: source

let untrusted_binaries

Stage 3: source

DeviceProcessEvents

Stage 4: where

where not (ActionType contains "aggregated")

Stage 5: where

where Timestamp > ago(3600s)

Stage 6: where

where InitiatingProcessCommandLine == "svchost.exe -k netsvcs -p -s Schedule"

Stage 7: distinct

distinct SHA1

Stage 8: join (negated)

join kind=leftanti (DeviceFileCertificateInfo) on SHA1

Stage 9: invoke

invoke

Stage 10: where

where IsCertificateValid != 1

Stage 11: where

where isnotempty(GlobalPrevalence) and GlobalPrevalence < 1000

Stage 12: join

join (DeviceProcessEvents) on SHA1

Stage 13: summarize aggregation inside the join branch

summarize by SHA1

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ActionTypecontainsaggregatedexcludes:ActionType field:"ActionType" value:"aggregated"
max_trustedeq1excludes:max_trusted field:"max_trusted" value:"1"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SHA1summarize

Jira - Workflow scheme copied

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

'Detects when workflow scheme was copied.'

MITRE ATT&CK Coverage

Rule Body

id: 398aa0ca-45a2-4f79-bc21-ee583bbb63bc
name: Jira - Workflow scheme copied
description: |
  'Detects when workflow scheme was copied.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: JiraAuditAPI
    dataTypes:
      - JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1213
query: |
  JiraAudit
  | where EventMessage =~ 'Workflow scheme copied'
  | extend workflow = todynamic(AssociatedItems)[0]['name']
  | project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, workflow
  | extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

JiraAudit

Stage 2: where

| where EventMessage =~ 'Workflow scheme copied'

Stage 3: extend

| extend workflow = todynamic(AssociatedItems)[0]['name']

Stage 4: project

| project EventCreationTime, ObjectItemName, UserName, SrcIpAddr, workflow

Stage 5: extend

| extend AccountCustomEntity = UserName, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventCreationTimeproject
ObjectItemNameproject
SrcIpAddrproject
UserNameproject
workflowproject
AccountCustomEntityextend
IPCustomEntityextend

Large number of AD objects accessed by user

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

This query detects a user accessing a large number of Group and User objects from Active Directory which is outside the baseline of normal behavior for that particular user.

Known False Positives

  • There are legitimate use-cases for downloading large number of AD objects. For example, an administrator might run a script to extract all users as part of certain migration activities.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days=14d; // Look back this many days to calculate baseline for maximum number of objects accessed per day.
let min_suspicious_count = 20000; // Only consider users looking up at least this many objects during the timeframe.
let suspicious_factor = 50; // Consider as suspicious if requesting more than suspicious_factor*daily maximum in lookback period.
let ADObjectAccess=(
    SecurityEvent
    | where EventID == 4662
    | where not(Account endswith "$")
    | where ObjectType in~ (
        "%{bf967aba-0de6-11d0-a285-00aa003049e2}", // User.
        "%{bf967a9c-0de6-11d0-a285-00aa003049e2}" //  Group.
    )
);
let AccessBaseline=(
    ADObjectAccess
    | where TimeGenerated <= ago(timeframe)
    | where TimeGenerated >= ago(timeframe + lookback_days)
    | summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
    | summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);
ADObjectAccess
| where ingestion_time() >= ago(timeframe)
| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account
| where ObjectCount > min_suspicious_count
| join kind=leftouter AccessBaseline on Account
| extend PreviousMaxObjectPerDay=coalesce(PreviousMaxObjectPerDay,0)
// Calculate what is considered a suspicious number for the given user.
| extend SuspiciousThreshold=max_of(PreviousMaxObjectPerDay*suspicious_factor,min_suspicious_count)
| where ObjectCount > SuspiciousThreshold
| project TimeGenerated, Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold
| extend AccountName=iif(Account contains @"\",tostring(split(Account,@"\")[1]),Account),AccountDomain=iif(Account contains @"\",tostring(split(Account,@"\")[0]),"")
// Begin environment-specific filter.
// End environment-specific filter.
// Begin de-duplication logic.
| extend DedupFieldValues=pack_all()
| mv-apply e=DedupFields to typeof(string) on (
    extend DedupValue=DedupFieldValues[tostring(e)]
    | order by e // Sorting is required to ensure make_list is deterministic.
    | summarize DedupValues=make_list(DedupValue)
)
| extend DedupEntity=strcat_array(DedupValues, "|")
| project-away DedupFieldValues, DedupValues
| join kind=leftanti (
    SecurityAlert
    | where AlertName has RuleId and ProviderName has "ASI"
    | where TimeGenerated >= ago(timeframe)
    | extend DedupEntity = tostring(parse_json(tostring(parse_json(ExtendedProperties)["Custom Details"])).DedupEntity[0])
    | project DedupEntity
) on DedupEntity
// End de-duplication logic.

Stages and Predicates

Parameters

let timeframe = 2*1d;
let RuleId = "0108";
let DedupFields = dynamic(["TimeGenerated"]);
let lookback_days = 14d;
let min_suspicious_count = 20000;
let suspicious_factor = 50;

let ADObjectAccess is inlined into the numbered stages below.

Let Binding: AccessBaseline used in Stages 2, 10

let AccessBaseline = (
    ADObjectAccess
    | where TimeGenerated <= ago(timeframe)
    | where TimeGenerated >= ago(timeframe + lookback_days)
    | summarize BaselineObjectCount=count() by Account, bin(TimeGenerated,1d)
    | summarize PreviousMaxObjectPerDay=max(BaselineObjectCount) by Account
);

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

Stage 1: source

let ADObjectAccess

Stage 2: source

let AccessBaseline

Stage 3: source

SecurityEvent

Stage 4: where

| where EventID == 4662

Stage 5: where

| where not(Account endswith "$")

Stage 6: where

| where ObjectType in~ (
        "%{bf967aba-0de6-11d0-a285-00aa003049e2}",
        "%{bf967a9c-0de6-11d0-a285-00aa003049e2}"
    )

Stage 7: where

ADObjectAccess
| where ingestion_time() >= ago(timeframe)

Stage 8: summarize

| summarize ObjectCount=count(),TimeGenerated=min(TimeGenerated) by Account

Stage 9: where

| where ObjectCount > min_suspicious_count

Stage 10: join

| join kind=leftouter AccessBaseline on Account

Stage 11: extend

extend PreviousMaxObjectPerDay

Stage 12: extend

extend SuspiciousThreshold

Stage 13: where

where ObjectCount > SuspiciousThreshold

Stage 14: project

project Account, ObjectCount, PreviousMaxObjectPerDay, SuspiciousThreshold, TimeGenerated

Stage 15: extend

extend AccountDomain, AccountName

Stage 16: extend

extend DedupFieldValues

Stage 17: kusto:mv-apply

kusto:mv-apply

Stage 18: extend

extend DedupEntity

Stage 19: project-away

project-away DedupFieldValues, DedupValues

Stage 20: join (negated)

join kind=leftanti (SecurityAlert) on DedupEntity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4662
field:"EventID" kind:eq value:"4662"
ObjectCountgt
  • 20000 transforms: number
field:"ObjectCount" kind:gt value:"20000"
ObjectTypein
  • %{bf967a9c-0de6-11d0-a285-00aa003049e2}
  • %{bf967aba-0de6-11d0-a285-00aa003049e2}
field:"ObjectType" kind:in

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
ObjectCountproject
PreviousMaxObjectPerDayproject
SuspiciousThresholdproject
TimeGeneratedproject
AccountDomainextend
AccountNameextend
DedupEntityextend

Microsoft Entra ID Hybrid Health AD FS Suspicious Application

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

This detection uses AzureActivity logs (Administrative category) to identify a suspicious application adding a server instance to an Microsoft Entra ID Hybrid Health AD FS service or deleting the AD FS service instance. Usually the Microsoft Entra ID Connect Health Agent application with ID cf6d7e68-f018-4e0a-a7b3-126e053fb88d and ID cb1056e2-e479-49de-ae31-7812af012ed8 is used to perform those operations.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: d9938c3b-16f9-444d-bc22-ea9a9110e0fd
name: Microsoft Entra ID Hybrid Health AD FS Suspicious Application
description: |
  'This detection uses AzureActivity logs (Administrative category) to identify a suspicious application adding a server instance to an Microsoft Entra ID Hybrid Health AD FS service or deleting the AD FS service instance.
  Usually the Microsoft Entra ID Connect Health Agent application with ID cf6d7e68-f018-4e0a-a7b3-126e053fb88d and ID cb1056e2-e479-49de-ae31-7812af012ed8 is used to perform those operations.'
severity: Medium
status: Available 
requiredDataConnectors:
  - connectorId: AzureActivity
    dataTypes:
      - AzureActivity
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1528
  - T1550
tags:
  - SimuLand
query: |
  // Microsoft Entra ID Connect Health Agent - cf6d7e68-f018-4e0a-a7b3-126e053fb88d
  // Microsoft Entra ID Connect - cb1056e2-e479-49de-ae31-7812af012ed8
  let appList = dynamic(['cf6d7e68-f018-4e0a-a7b3-126e053fb88d','cb1056e2-e479-49de-ae31-7812af012ed8']);
  let operationNamesList = dynamic(['Microsoft.ADHybridHealthService/services/servicemembers/action','Microsoft.ADHybridHealthService/services/delete']);
  AzureActivity
  | where CategoryValue =~ 'Administrative'
  | where ResourceProviderValue =~ 'Microsoft.ADHybridHealthService'
  | where _ResourceId has 'AdFederationService'
  | where OperationNameValue in~ (operationNamesList)
  | extend claimsJson = parse_json(Claims)
  | extend AppId = tostring(claimsJson.appid), AccountName = tostring(claimsJson.name), Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])
  | where AppId !in (appList)
  | project-away claimsJson
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Caller
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIpAddress
version: 2.0.3
kind: Scheduled

Stages and Predicates

Parameters

let appList = dynamic(['cf6d7e68-f018-4e0a-a7b3-126e053fb88d','cb1056e2-e479-49de-ae31-7812af012ed8']);
let operationNamesList = dynamic(['Microsoft.ADHybridHealthService/services/servicemembers/action','Microsoft.ADHybridHealthService/services/delete']);

Stage 1: source

AzureActivity

Stage 2: where

| where CategoryValue =~ 'Administrative'

Stage 3: where

| where ResourceProviderValue =~ 'Microsoft.ADHybridHealthService'

Stage 4: where

| where _ResourceId has 'AdFederationService'

Stage 5: where

| where OperationNameValue in~ (operationNamesList)

Stage 6: extend

| extend claimsJson = parse_json(Claims)

Stage 7: extend

| extend AppId = tostring(claimsJson.appid), AccountName = tostring(claimsJson.name), Name = tostring(split(Caller,'@',0)[0]), UPNSuffix = tostring(split(Caller,'@',1)[0])

Stage 8: where

| where AppId !in (appList)

Stage 9: project-away

| project-away claimsJson

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
CategoryValueeq
  • Administrative corpus 6 (kusto 4, sigma 2)
field:"CategoryValue" kind:eq value:"Administrative"
OperationNameValuein
  • Microsoft.ADHybridHealthService/services/delete
  • Microsoft.ADHybridHealthService/services/servicemembers/action
field:"azure_ad::operation_name_value" kind:in
ResourceProviderValueeq
  • Microsoft.ADHybridHealthService corpus 6 (kusto 4, sigma 2)
field:"ResourceProviderValue" kind:eq value:"Microsoft.ADHybridHealthService"
_ResourceIdmatch
  • AdFederationService transforms: term corpus 4 (kusto 4)
field:"_ResourceId" kind:match value:"AdFederationService"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AppIdincb1056e2-e479-49de-ae31-7812af012ed8, cf6d7e68-f018-4e0a-a7b3-126e053fb88dexcludes:AppId field:"AppId" value:"cb1056e2-e479-49de-ae31-7812af012ed8" field:"AppId" value:"cf6d7e68-f018-4e0a-a7b3-126e053fb88d"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameextend
AppIdextend
Nameextend
UPNSuffixextend

New High Severity Vulnerability Detected Across Multiple Hosts

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

'This creates an incident when a new high severity vulnerability is detected across multilple hosts'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 6116dc19-475a-4148-84b2-efe89c073e27
name: New High Severity Vulnerability Detected Across Multiple Hosts
description: |
  'This creates an incident when a new high severity vulnerability is detected across multilple hosts'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: QualysVMLogsCCPDefinition
    dataTypes:
      - QualysHostDetection
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  let threshold = 10;
  QualysHostDetection
  | extend Status = tostring(Status), Vulnerability = tostring(QID), Severity = tostring(Severity)
  | where Status =~ "New" and Severity == "5"
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(NetBios) by tostring(QID)
  | where dcount_NetBios >= threshold
  | extend timestamp = StartTime
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;

Stage 1: source

QualysHostDetection

Stage 2: extend

| extend Status = tostring(Status), Vulnerability = tostring(QID), Severity = tostring(Severity)

Stage 3: where

| where Status =~ "New" and Severity == "5"

Stage 4: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), dcount(NetBios) by tostring(QID)

Stage 5: where

| where dcount_NetBios >= threshold

Stage 6: extend

| extend timestamp = StartTime

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
StartTimesummarize
timestampextend

TI Map URL Entity to DeviceNetworkEvents

#
Severity
medium
Time window
14d
Group by
$left.Url, $right.RemoteUrl, IndicatorId, Url
Source
github.com/Azure/Azure-Sentinel

This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: 6ddbd892-a9be-47be-bab7-521241695bd6
name: TI Map URL Entity to DeviceNetworkEvents
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let DeviceNetworkEvents_ = DeviceNetworkEvents
    | where isnotempty(RemoteUrl)
    | where TimeGenerated >= ago(dt_lookBack)
    | where ActionType !has "ConnectionFailed"
    | extend RemoteUrl = tolower(RemoteUrl)
    | project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
  let DeviceNetworkEventUrls = DeviceNetworkEvents_
    | distinct Url = RemoteUrl
    | summarize make_list(Url);
  ThreatIntelligenceIndicator
  | where isnotempty(Url)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend Url = tolower(Url)
  | where Url in (DeviceNetworkEventUrls)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  | join kind=innerunique (DeviceNetworkEvents_) on $left.Url == $right.RemoteUrl
  | where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime
  | summarize DeviceNetworkEvents_TimeGenerated = arg_max(DeviceNetworkEvents_TimeGenerated, *) by IndicatorId, Url
  | project DeviceNetworkEvents_TimeGenerated, IndicatorId, Url, ConfidenceScore, Description, ThreatType, Tags, TrafficLightProtocolLevel, ActionType, DeviceId, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort
  | extend Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
  | extend timestamp = DeviceNetworkEvents_TimeGenerated, UserPrincipalName = InitiatingProcessAccountUpn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: DeviceNetworkEvents_ used in Stages 1, 11

let DeviceNetworkEvents_ = DeviceNetworkEvents
  | where isnotempty(RemoteUrl)
  | where TimeGenerated >= ago(dt_lookBack)
  | where ActionType !has "ConnectionFailed"
  | extend RemoteUrl = tolower(RemoteUrl)
  | project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;

Let Binding: DeviceNetworkEventUrls used in Stages 2, 7

let DeviceNetworkEventUrls = DeviceNetworkEvents_
  | distinct Url = RemoteUrl
  | summarize make_list(Url);

Stage 1: source

let DeviceNetworkEvents_

Stage 2: source

let DeviceNetworkEventUrls

Stage 3: source

ThreatIntelligenceIndicator

Stage 4: where

where isnotempty(Url)

Stage 5: where

where TimeGenerated >= ago(1209600s)

Stage 6: extend

extend Url

Stage 7: where

where Url == "DeviceNetworkEventUrls"

Stage 8: summarize

summarize LatestIndicatorTime by IndicatorId

Stage 9: where

where Active == true

Stage 10: where

where not (Description contains_cs "State: falsepos;") and not (Description contains_cs "State: inactive;")

Stage 11: join

join kind=innerunique (DeviceNetworkEvents_) on $left.Url, $right.RemoteUrl

Stage 12: where

where DeviceNetworkEvents_TimeGenerated < ExpirationDateTime

Stage 13: summarize

summarize DeviceNetworkEvents_TimeGenerated by IndicatorId, Url

Stage 14: project

project ActionType, ConfidenceScore, Description, DeviceId, DeviceName, DeviceNetworkEvents_TimeGenerated, IndicatorId, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort, Tags, ThreatType, TrafficLightProtocolLevel, Url

Stage 15: extend

extend Name, UPNSuffix

Stage 16: extend

extend UserPrincipalName, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true
field:"Active" kind:eq value:"true"
RemoteUrlis_not_null
  • (no value, null check)
field:"RemoteUrl" kind:is_not_null
Urlin
  • DeviceNetworkEventUrls
field:"Url" kind:in value:"DeviceNetworkEventUrls"
Urlis_not_null
  • (no value, null check)
field:"Url" kind:is_not_null

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionTypeproject
ConfidenceScoreproject
Descriptionproject
DeviceIdproject
DeviceNameproject
DeviceNetworkEvents_TimeGeneratedproject
IndicatorIdproject
InitiatingProcessAccountUpnproject
InitiatingProcessCommandLineproject
RemoteIPproject
RemotePortproject
Tagsproject
ThreatTypeproject
TrafficLightProtocolLevelproject
Urlproject
Nameextend
UPNSuffixextend
UserPrincipalNameextend
timestampextend

TI Map URL Entity to OfficeActivity Data [Deprecated]

#
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

'This query is Deprecated as its filter conditions will never yield results. This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 36a9c9e5-3dc1-4ed9-afaa-1d13617bfc2b
name: TI Map URL Entity to OfficeActivity Data [Deprecated]
description: |
  'This query is Deprecated as its filter conditions will never yield results. This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in OfficeActivity data.'
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
     - OfficeActivity
  - connectorId: ThreatIntelligence
    dataTypes:
     - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  // let ioc_lookBack = 14d;
  // ThreatIntelligenceIndicator
  // // Picking up only IOC's that contain the entities we want
  // | where isnotempty(Url)
  // | where TimeGenerated >= ago(ioc_lookBack)
  // | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  // | where Active == true and ExpirationDateTime > now()
  // // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  // | join kind=innerunique (
  // OfficeActivity
  // | where TimeGenerated >= ago(dt_lookBack)
  // //Extract the Url from a number of potential fields
  // | extend Url = iif(OfficeWorkload == "AzureActiveDirectory",extract("(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)", 1,ModifiedProperties),tostring(parse_json(ModifiedProperties)[12].NewValue))
  // | where isnotempty(Url)
  // // Ensure we get a clean URL
  // | extend Url = tostring(split(Url, ';')[0])
  // | extend OfficeActivity_TimeGenerated = TimeGenerated
  // // Project a single user identity that we can use for entity mapping
  // | extend User = iif(isnotempty(UserId), UserId, iif(isnotempty(Actor), tostring(parse_json(Actor)[0].ID), tostring(parse_json(Parameters)[0].Value)))
  // ) on Url
  // | where OfficeActivity_TimeGenerated < ExpirationDateTime
  // | summarize OfficeActivity_TimeGenerated = arg_max(OfficeActivity_TimeGenerated, *) by IndicatorId, Url
  // | project OfficeActivity_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, Operation,
  // UserType, OfficeWorkload, Parameters, Url, User
  // | extend timestamp = OfficeActivity_TimeGenerated, Name = tostring(split(User, '@', 0)[0]), UPNSuffix = tostring(split(User, '@', 1)[0])
  datatable() []
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.9
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;

Stage 1: source

datatable() []

Unauthorized device in the network (Microsoft Defender for IoT)

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

'This alert leverages Defender for IoT to detect a new device indicating a legitimate device recently installed on the network or an indication of malicious activity such as a cyber threat attempting to manipulate the SCADA network.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: f4c71e55-6192-47ca-92e2-0856ae502a46
name: Unauthorized device in the network (Microsoft Defender for IoT)
description: |
  'This alert leverages Defender for IoT to detect a new device indicating a legitimate device recently installed on the network or an indication of malicious activity such as a cyber threat attempting to manipulate the SCADA network.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: IoT
    dataTypes:
      - SecurityAlert (ASC for IoT)
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T0842
query: |
  SecurityAlert
  | where ProviderName == "IoTSecurity"
  | where AlertName in ("Field Device Discovered Unexpectedly", "New Asset Detected", "Abnormal usage of MAC Addresses", "New LLDP Device Configuration")
  | extend ExtendedProperties = parse_json(ExtendedProperties)
  | where tostring(ExtendedProperties.isNew) == "True"
  | extend DeviceId = tostring(ExtendedProperties.DeviceId), 
           SourceDeviceAddress = tostring(ExtendedProperties.SourceDeviceAddress), 
           DestDeviceAddress = tostring(ExtendedProperties.DestinationDeviceAddress), 
           RemediationSteps = tostring(parse_json(RemediationSteps)[0]), 
           Protocol = tostring(ExtendedProperties.Protocol), 
           AlertManagementUri = tostring(ExtendedProperties.AlertManagementUri)
  | project
    TimeGenerated,
    DeviceId,
    ProductName,
    ProductComponentName,
    AlertSeverity,
    AlertName,
    Description,
    Protocol,
    SourceDeviceAddress,
    DestDeviceAddress,
    RemediationSteps,
    Tactics,
    Entities,
    VendorOriginalId,
    AlertLink,
    AlertManagementUri,
    Techniques
entityMappings:
sentinelEntitiesMappings:
  - columnName: Entities
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  Sensor: DeviceId
  Protocol: Protocol
  VendorOriginalId: VendorOriginalId
  AlertManagementUri: AlertManagementUri
alertDetailsOverride:
  alertDisplayNameFormat: (MDIoT) {{AlertName}}
  alertDescriptionFormat: (MDIoT) {{Description}}
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: AlertSeverity
  alertDynamicProperties: 
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: RemediationSteps
      value: RemediationSteps
    - alertProperty: Techniques
      value: Techniques
    - alertProperty: ProductComponentName
      value: ProductComponentName
    - alertProperty: AlertLink
      value: AlertLink
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityAlert

Stage 2: where

| where ProviderName == "IoTSecurity"

Stage 3: where

| where AlertName in ("Field Device Discovered Unexpectedly", "New Asset Detected", "Abnormal usage of MAC Addresses", "New LLDP Device Configuration")

Stage 4: extend

| extend ExtendedProperties = parse_json(ExtendedProperties)

Stage 5: where

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

Stage 6: extend

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

Stage 7: project

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AlertNamein
  • Abnormal usage of MAC Addresses
  • Field Device Discovered Unexpectedly
  • New Asset Detected
  • New LLDP Device Configuration
field:"AlertName" kind:in
ExtendedProperties.isNeweq
  • True transforms: tostring, kusto_dynamic_path corpus 15 (kusto 15)
field:"ExtendedProperties.isNew" kind:eq value:"True"
ProviderNameeq
  • IoTSecurity corpus 15 (kusto 15)
field:"ProviderName" kind:eq value:"IoTSecurity"

Output Fields

These fields are emitted when the rule matches.

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

UniFi Site Manager: ISP High Latency

#
Status
available
Severity
medium
Time window
30m
Group by
SiteId, ispAsn, ispName, metricTime
Source
github.com/Azure/Azure-Sentinel

Identifies when ISP latency exceeds acceptable thresholds, impacting VoIP quality, video conferencing, and overall application performance.

MITRE ATT&CK Coverage

Rule Body

id: 77d8f6d0-b45c-ea6e-7c58-daac194a095f
name: "UniFi Site Manager: ISP High Latency"
description: |
  Identifies when ISP latency exceeds acceptable thresholds, impacting VoIP quality, video conferencing, and overall application performance.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 15m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1498
  - T1499
subTechniques:
  - T1498.001
  - T1499.002
query: |
  // UniFi ISP High Latency Detection
  let AvgLatencyThreshold = 200;
  let MaxLatencyThreshold = 1000;
  let SustainedPeriodCount = 2;
  Unifi_SiteManager_ISPMetrics_CL
  | where TimeGenerated > ago(30m)
  | mv-expand period = Periods
  | extend
      metricTime = todatetime(period.metricTime),
      avgLatency = toint(period.data.wan.avgLatency),
      maxLatency = toint(period.data.wan.maxLatency),
      ispName = tostring(period.data.wan.ispName),
      ispAsn = tostring(period.data.wan.ispAsn)
  // De-duplicate Periods: each poll returns the same hour buckets, so collapse to latest value per metricTime
  | summarize arg_max(TimeGenerated, avgLatency, maxLatency, ispAsn) by tostring(SiteId), ispName, metricTime
  | where metricTime > ago(30m)
  | where avgLatency > AvgLatencyThreshold or maxLatency > MaxLatencyThreshold
  | summarize
      EventCount = count(),
      AvgLatency = avg(avgLatency),
      MaxLatency = max(maxLatency),
      FirstSeen = min(metricTime),
      LastSeen = max(metricTime)
      by SiteId, ispName, ispAsn
  | where EventCount >= SustainedPeriodCount
  | extend 
      TimeGenerated = now(),
      AvgLatencyRounded = round(AvgLatency, 1)
  | project
      TimeGenerated,
      SiteId = SiteId,
      ISPName = ispName,
      ISPAsn = ispAsn,
      AvgLatencyMs = AvgLatencyRounded,
      MaxLatencyMs = MaxLatency,
      EventCount,
      FirstSeen,
      LastSeen
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ISPName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT4H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Parameters

let AvgLatencyThreshold = 200;
let MaxLatencyThreshold = 1000;
let SustainedPeriodCount = 2;

Stage 1: source

Unifi_SiteManager_ISPMetrics_CL

Stage 2: where

| where TimeGenerated > ago(30m)

Stage 3: mv-expand

| mv-expand period = Periods

Stage 4: extend

| extend
    metricTime = todatetime(period.metricTime),
    avgLatency = toint(period.data.wan.avgLatency),
    maxLatency = toint(period.data.wan.maxLatency),
    ispName = tostring(period.data.wan.ispName),
    ispAsn = tostring(period.data.wan.ispAsn)

Stage 5: summarize

| summarize arg_max(TimeGenerated, avgLatency, maxLatency, ispAsn) by tostring(SiteId), ispName, metricTime

Stage 6: where

| where metricTime > ago(30m)

Stage 7: where

| where avgLatency > AvgLatencyThreshold or maxLatency > MaxLatencyThreshold

Stage 8: summarize

| summarize
    EventCount = count(),
    AvgLatency = avg(avgLatency),
    MaxLatency = max(maxLatency),
    FirstSeen = min(metricTime),
    LastSeen = max(metricTime)
    by SiteId, ispName, ispAsn

Stage 9: where

| where EventCount >= SustainedPeriodCount

Stage 10: extend

| extend 
    TimeGenerated = now(),
    AvgLatencyRounded = round(AvgLatency, 1)

Stage 11: project

| project
    TimeGenerated,
    SiteId = SiteId,
    ISPName = ispName,
    ISPAsn = ispAsn,
    AvgLatencyMs = AvgLatencyRounded,
    MaxLatencyMs = MaxLatency,
    EventCount,
    FirstSeen,
    LastSeen

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvgLatencyMsproject
EventCountproject
FirstSeenproject
ISPAsnproject
ISPNameproject
LastSeenproject
MaxLatencyMsproject
SiteIdproject
TimeGeneratedproject

Vulerabilities

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

This query searches for all the vulerabilities that RidgeBot identified

MITRE ATT&CK Coverage

Rule Body

id: d096643d-6789-4c74-8893-dd3fc8a94069
name: Vulerabilities
description: |
  This query searches for all the vulerabilities that RidgeBot identified
severity: High
status: Available
requiredDataConnectors: 
  - connectorId: RidgeBotDataConnector
    dataTypes:
      - CommonSecurityLog
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 12h
queryPeriod: 12h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - InitialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1189
  - T1059
  - T1053
  - T1548
query: |
  CommonSecurityLog
  | where DeviceVendor == "RidgeSecurity"
  | where DeviceEventClassID startswith "40"
  | order by LogSeverity
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: DeviceVendor
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Computer
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "RidgeSecurity"

Stage 3: where

| where DeviceEventClassID startswith "40"

Stage 4: sort

| order by LogSeverity

Indicators

These rows show field, operator, and value matches.

Back to all Kusto rules