Detection rules › Kusto

Kusto rules: 2y

AWSCloudTrail - Creation of DataPipeline policy and then privilege escalation

#
Status
available
Severity
high
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 combine AWS Data Pipeline permissions with IAM privileges such as iam:PassRole, followed by attachment activity to users, roles, or groups. This sequence can enable escalation paths and should be reviewed for unauthorized privilege assignment.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 6009c632-94e9-4ffb-a11a-b4b99f457f88
name: AWSCloudTrail - Creation of DataPipeline policy and then privilege escalation
description: |
  Identifies creation of IAM policies that combine AWS Data Pipeline permissions with IAM privileges such as
  iam:PassRole, followed by attachment activity to users, roles, or groups. This sequence can enable escalation
  paths and should be reviewed for unauthorized privilege assignment.
severity: High
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:*" or Action contains "iam:PassRole") and Action contains "datapipeline:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:CreatePipeline" and Action contains "datapipeline:PutPipelineDefinition" and Action contains "datapipeline:ActivatePipeline") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:Create*" and Action contains "datapipeline:Put*" and Action contains "datapipeline:Activate*")) and Resource == "*" and Condition == ""
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn,  RecipientAccountId, AccountName, AccountUPNSuffix
  | 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,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, 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, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
  );
  // 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 Data Pipeline privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.4
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,   UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, 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, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
);

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:*" or Action contains "iam:PassRole") and Action contains "datapipeline:*") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:CreatePipeline" and Action contains "datapipeline:PutPipelineDefinition" and Action contains "datapipeline:ActivatePipeline") or ((Action contains "iam:*" or Action contains "iam:PassRole") and Action contains "datapipeline:Create*" and Action contains "datapipeline:Put*" and Action contains "datapipeline:Activate*")) and Resource == "*" and Condition == ""

Stage 13: distinct

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

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, UserIdentityType, UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • datapipeline:*
  • datapipeline:Activate*
  • datapipeline:ActivatePipeline
  • datapipeline:Create*
  • datapipeline:CreatePipeline
  • datapipeline:Put*
  • datapipeline:PutPipelineDefinition
  • iam:*
  • iam:PassRole
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

Cisco SE - Unexpected binary file

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

'Detects binary files in uncommon locations.'

MITRE ATT&CK Coverage

Rule Body

id: eabb9c20-7b0b-4a77-81e8-b06944f351c6
name: Cisco SE - Unexpected binary file
description: |
  'Detects binary files in uncommon locations.'
severity: Medium
requiredDataConnectors:
  - connectorId: CiscoSecureEndpoint
    dataTypes:
      - CiscoSecureEndpoint
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  CiscoSecureEndpoint
  | where isnotempty(SrcFileName)
  | where SrcFilePath contains @'AppData\Local\Temp' or SrcFilePath contains @'OUTLOOK_TEMP'
  | extend ext = extract(@"(.*\/)?(.*)", 2, tostring(SrcFilePath))
  | where ext contains 'dll' or ext contains @'ps1' or ext contains @'exe'
  | extend HostCustomEntity = DstHostname
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CiscoSecureEndpoint

Stage 2: where

| where isnotempty(SrcFileName)

Stage 3: where

| where SrcFilePath contains @'AppData\Local\Temp' or SrcFilePath contains @'OUTLOOK_TEMP'

Stage 4: extend

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

Stage 5: where

| where ext contains 'dll' or ext contains @'ps1' or ext contains @'exe'

Stage 6: extend

| extend HostCustomEntity = DstHostname

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
SrcFileNameis_not_null
  • (no value, null check)
field:"SrcFileName" kind:is_not_null
SrcFilePathcontains
  • AppData\Local\Temp
  • OUTLOOK_TEMP
field:"SrcFilePath" kind:contains
extcontains
  • dll
  • exe
  • ps1
field:"ext" kind:contains

Output Fields

These fields are emitted when the rule matches.

FieldSource
extextend
HostCustomEntityextend

CiscoISE - Device changed IP in last 24 hours

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

'Detects when device changes IP address in last 24 hours.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 0c509e9b-121e-4951-9f9b-43722e052b4f
name: CiscoISE - Device changed IP in last 24 hours
description: |
  'Detects when device changes IP address in last 24 hours.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1568
query: |
  let lbtime_48h = 48h;
  let lbtime_24h = 24h;
  CiscoISEEvent
  | where TimeGenerated between (ago(lbtime_48h) .. ago(lbtime_24h))
  | where notempty(DvcIpAddr) and notempty(DvcHostname)
  | summarize knownIPs = make_set(DvcIpAddr) by DvcHostname
  | join (CiscoISEEvent
        | where TimeGenerated > ago(lbtime_24h)
        | where notempty(DvcIpAddr) and notempty(DvcHostname)
        | summarize evts = count() by DvcHostname, DvcIpAddr
        | project-away evts) on DvcHostname
  | project-away DvcHostname1
  | where knownIPs !contains DvcIpAddr
  | extend HostCustomEntity = DvcHostname
  | extend IPCustomEntity = DvcIpAddr
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: HostCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let lbtime_48h = 48h;
let lbtime_24h = 24h;

Stage 1: source

CiscoISEEvent

Stage 2: where

| where TimeGenerated between (ago(lbtime_48h) .. ago(lbtime_24h))

Stage 3: where

| where notempty(DvcIpAddr) and notempty(DvcHostname)

Stage 4: summarize

| summarize knownIPs = make_set(DvcIpAddr) by DvcHostname

Stage 5: join

| join (CiscoISEEvent
      | where TimeGenerated > ago(lbtime_24h)
      | where notempty(DvcIpAddr) and notempty(DvcHostname)
      | summarize evts = count() by DvcHostname, DvcIpAddr
      | project-away evts) on DvcHostname

Stage 6: project-away

project-away DvcHostname1

Stage 7: where

where knownIPs !contains DvcIpAddr

Stage 8: extend

extend HostCustomEntity

Stage 9: extend

extend IPCustomEntity

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DvcHostnameis_not_null
  • (no value, null check)
field:"DvcHostname" kind:is_not_null
DvcIpAddris_not_null
  • (no value, null check)
field:"DvcIpAddr" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
DvcHostnamesummarize
knownIPssummarize
HostCustomEntityextend
IPCustomEntityextend

CTE - Possible Company-Wide C2 Campaign (Multiple Devices, Same Destination)

#
Severity
high
Time window
1d
Group by
Pattern, TenantId, Url
Source
github.com/Azure/Azure-Sentinel

Multiple distinct devices within the same tenant were blocked attempting to reach the same URL/domain within a 24-hour window. Isolated blocks are usually just one user's browsing; several independent devices converging on the same destination is a strong signal of a coordinated malware/C2 campaign spreading across the company (e.g. a phishing link opened by several employees, or malware propagating internally).

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement
Command & Control

Rule Body

id: ec08477d-87a1-498d-8f34-72b8ec8f3758
name: CTE - Possible Company-Wide C2 Campaign (Multiple Devices, Same Destination)
description: |
  Multiple distinct devices within the same tenant were blocked attempting to reach the same URL/domain within a 24-hour window. Isolated blocks are usually just one user's browsing; several independent devices converging on the same destination is a strong signal of a coordinated malware/C2 campaign spreading across the company (e.g. a phishing link opened by several employees, or malware propagating internally).
severity: High
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 30m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - LateralMovement
relevantTechniques:
  - T1071
  - T1570
query: |
  CybereinforceCTE_CL
  | where EventType == "block.url"
  | extend d = parse_json(Details)
  | extend
      DeviceId = tostring(d.deviceId),
      HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
      Pattern = tostring(d.pattern),
      Url = tostring(d.url),
      IsIoc = tobool(d.ioc),
      ClientIp = tostring(split(tostring(d.clientIp), ":")[0])
  | summarize DeviceCount=dcount(DeviceId), AffectedHosts=make_set(HostName, 50), SampleIps=make_set(ClientIp, 20), AnyIoc=max(IsIoc), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Pattern, Url, TenantId
  | where DeviceCount >= 3
  | extend ClientIp = tostring(SampleIps[0])
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "block.url"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend
    DeviceId = tostring(d.deviceId),
    HostName = iff(isnotempty(tostring(d.hostname)), tostring(d.hostname), strcat("cte-device-", substring(EntityId, 0, 8))),
    Pattern = tostring(d.pattern),
    Url = tostring(d.url),
    IsIoc = tobool(d.ioc),
    ClientIp = tostring(split(tostring(d.clientIp), ":")[0])
HostName =
ifisnotempty(tostring(d.hostname))tostring(d.hostname)
elsestrcat("cte-device-", substring(EntityId, 0, 8))

Stage 5: summarize

| summarize DeviceCount=dcount(DeviceId), AffectedHosts=make_set(HostName, 50), SampleIps=make_set(ClientIp, 20), AnyIoc=max(IsIoc), FirstSeen=min(TimeGenerated), LastSeen=max(TimeGenerated) by Pattern, Url, TenantId

Stage 6: where

| where DeviceCount >= 3

Stage 7: extend

| extend ClientIp = tostring(SampleIps[0])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AffectedHostssummarize
AnyIocsummarize
DeviceCountsummarize
FirstSeensummarize
LastSeensummarize
Patternsummarize
SampleIpssummarize
TenantIdsummarize
Urlsummarize
ClientIpextend

Cyren High-Risk IP Indicators

#

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
1d
Group by
IP, Source
Source
github.com/Azure/Azure-Sentinel

'Detects high-risk IP indicators (risk score >= 80) from Cyren threat intelligence feeds in the last 24 hours. These IPs are associated with malicious activity such as malware distribution, phishing, or botnet command and control.'

MITRE ATT&CK Coverage

Rule Body

id: 5d7e8b3a-1f2c-4e5d-9a0b-c1d2e3f4a5b6
name: Cyren High-Risk IP Indicators
description: |
  'Detects high-risk IP indicators (risk score >= 80) from Cyren threat intelligence feeds in the last 24 hours.
  These IPs are associated with malicious activity such as malware distribution, phishing, or botnet command and control.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CyrenThreatIntel
    dataTypes:
      - Cyren_Indicators_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Impact
relevantTechniques:
  - T1071
  - T1568
query: |
  Cyren_Indicators_CL
  | where TimeGenerated > ago(1d)
  | where isnotempty(ip_s)
  | extend Risk = toint(risk_d)
  | where Risk >= 80
  | summarize 
      DetectionCount = count(), 
      MaxRisk = max(Risk), 
      Categories = make_set(category_s) 
    by IP = ip_s, Source = source_s
  | where DetectionCount >= 1
  | extend 
      IPAddress = IP,
      ThreatCategories = strcat_array(Categories, ", ")
suppressionDuration: 1h
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1d
    matchingMethod: Selected
    groupByEntities:
      - IP
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  RiskScore: MaxRisk
  DetectionCount: DetectionCount
  Categories: ThreatCategories
  Source: Source
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Cyren_Indicators_CL

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where isnotempty(ip_s)

Stage 4: extend

| extend Risk = toint(risk_d)

Stage 5: where

| where Risk >= 80

Stage 6: summarize

| summarize 
    DetectionCount = count(), 
    MaxRisk = max(Risk), 
    Categories = make_set(category_s) 
  by IP = ip_s, Source = source_s

Stage 7: where

| where DetectionCount >= 1

Stage 8: extend

| extend 
    IPAddress = IP,
    ThreatCategories = strcat_array(Categories, ", ")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DetectionCountge
  • 1 transforms: number
field:"DetectionCount" kind:ge value:"1"
Riskge
  • 80 transforms: number
field:"Risk" kind:ge value:"80"
ip_sis_not_null
  • (no value, null check)
field:"ip_s" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoriessummarize
DetectionCountsummarize
IPsummarize
MaxRisksummarize
Sourcesummarize
IPAddressextend
ThreatCategoriesextend

Detect Unknown process launched via WinRM

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

When an unknown process is being launched from the WinRM service on a server, this might indicate a malicious actor spreading malware on various servers via the WinRM protocol.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

DeviceProcessEvents
| where InitiatingProcessFileName contains "wsmprovhost.exe"
| invoke FileProfile(SHA1)
| where GlobalPrevalence < 1000
| join kind=leftouter (
    DeviceNetworkEvents
    | where ActionType == "InboundConnectionAccepted"
    | where LocalPort in ("5985", "5986")
    | distinct RemoteIP, DeviceId
) on DeviceId
| project-away DeviceId1

Stages and Predicates

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where InitiatingProcessFileName contains "wsmprovhost.exe"

Stage 3: invoke

| invoke FileProfile(SHA1)

Stage 4: where

| where GlobalPrevalence < 1000

Stage 5: join

| join kind=leftouter (
    DeviceNetworkEvents
    | where ActionType == "InboundConnectionAccepted"
    | where LocalPort in ("5985", "5986")
    | distinct RemoteIP, DeviceId
) on DeviceId

Stage 6: project-away

project-away DeviceId1

Indicators

These rows show field, operator, and value matches.

Digital Guardian - Sensitive data transfer over insecure channel

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

'Detects sensitive data transfer over insecure channel.'

MITRE ATT&CK Coverage

Rule Body

id: b52cda18-c1af-40e5-91f3-1fcbf9fa267e
name: Digital Guardian - Sensitive data transfer over insecure channel
description: |
  'Detects sensitive data transfer over insecure channel.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1048
query: |
  DigitalGuardianDLPEvent
  | where isnotempty(MatchedPolicies)
  | where isnotempty(inspected_document)
  | where NetworkApplicationProtocol =~ 'HTTP'
  | extend AccountCustomEntity = SrcUserName, IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

DigitalGuardianDLPEvent

Stage 2: where

| where isnotempty(MatchedPolicies)

Stage 3: where

| where isnotempty(inspected_document)

Stage 4: where

| where NetworkApplicationProtocol =~ 'HTTP'

Stage 5: extend

| extend AccountCustomEntity = SrcUserName, IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
IPCustomEntityextend

Disable or Modify Windows Defender

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

This detection watches the commandline logs for known commands that are used to disable the Defender AV. This is based on research performed by @olafhartong on a large sample of malware for varying purposes. Note that this detection is imperfect and is only meant to serve as basis for building a more resilient detection rule. Make the detection more resilient, currently the order of parameters matters. You don't want that for a production rule. See blogpost (https://medium.com/falconforce/falconfriday-av-manipulation-0xff0e-67ed4387f9ab?source=friends_link&sk=3c7c499797bbb4d74879e102ef3ecf8f) for more resilience considerations. The current approach can easily be bypassed by not using the powershell.exe executable. Consider adding more ways to detect this behavior.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 20d52a04-b5d8-402d-88e2-7929d12cbdcd
name: Disable or Modify Windows Defender
description: |
  This detection watches the commandline logs for known commands that are used to disable the Defender AV. This is based on research performed by @olafhartong on a large sample of malware for varying purposes. 
  Note that this detection is imperfect and is only meant to serve as basis for building a more resilient detection rule. Make the detection more resilient, currently the order of parameters matters. You don't want that for a production rule. 
  See blogpost (https://medium.com/falconforce/falconfriday-av-manipulation-0xff0e-67ed4387f9ab?source=friends_link&sk=3c7c499797bbb4d74879e102ef3ecf8f) for more resilience considerations. The current approach can easily be bypassed by not using the powershell.exe executable. 
  Consider adding more ways to detect this behavior.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562.001
query: |
  let defendertampering=dynamic(["Set-MpPreference -DisableRealtimeMonitoring $true","sc stop WinDefend","sc delete WinDefend","Set-MpPreference -DisableBehaviorMonitoring $true","Set-MpPreference -ExclusionProcess", "Set-MpPreference -ExclusionExtension dll","net stop security center"]);
  DeviceProcessEvents
  | where ProcessCommandLine has_any (defendertampering)
  // If you have a lot of false positives coming from JetBrains, you can use the line below. 
  //| where InitiatingProcessFolderPath !startswith @"c:\program files\jetbrains\" and InitiatingProcessVersionInfoProductName !~ ("Android Studio")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: AccountSid 
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: defendertampering used in Stage 2

let defendertampering = dynamic(["Set-MpPreference -DisableRealtimeMonitoring $true","sc stop WinDefend","sc delete WinDefend","Set-MpPreference -DisableBehaviorMonitoring $true","Set-MpPreference -ExclusionProcess", "Set-MpPreference -ExclusionExtension dll","net stop security center"]);

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where ProcessCommandLine has_any (defendertampering)

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ProcessCommandLinematch
  • Set-MpPreference -DisableBehaviorMonitoring $true transforms: term
  • Set-MpPreference -DisableRealtimeMonitoring $true transforms: term
  • Set-MpPreference -ExclusionExtension dll transforms: term
  • Set-MpPreference -ExclusionProcess transforms: term
  • net stop security center transforms: term
  • sc delete WinDefend transforms: term
  • sc stop WinDefend transforms: term
field:"CommandLine" kind:match

Guardian- Racial Bias Policy Violation Detection

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

'This alert creates an incident when Racial Bias Policy Violation detected from the Guardian.'

Rule Body

id: 46103101-43d9-4c09-b8c8-898dcafe73c0
name: Guardian- Racial Bias Policy Violation Detection
description: |
  'This alert creates an incident when Racial Bias Policy Violation detected from the Guardian.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Racial Bias'
  | where Severity =~ 'Low'
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: Guardian- Racial Bias Policy Violation detection
  alertDescriptionFormat: |
    This query detects Racial Bias Policy Violation detected from the Guardian generated at {{TimeGenerated}}.\n\nPlease check the source for more information and investigate further.
  alertTacticsColumnName: null
  alertSeverityColumnName: Severity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Racial Bias'

Stage 3: where

| where Severity =~ 'Low'

Indicators

These rows show field, operator, and value matches.

NordPass - Activity token revocation

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

This will alert you when the event reporting token is revoked, posing the risk of active integration being blocked.

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Rule Body

id: 800314a6-759a-4575-93e2-1e080b1d33f9
name: NordPass - Activity token revocation
kind: Scheduled
version: 1.0.0
displayName: Activity token revocation
description: This will alert you when the event reporting token is revoked, posing the risk of active integration being blocked.
severity: Medium
query: |
  NordPassEventLogs_CL
  | where event_type == "integrations"
  | where action == "activities_token_revoked"
  | extend TargetEmail = user_email
requiredDataConnectors:
  - connectorId: NordPass
    dataTypes:
      - NordPassEventLogs_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1134
incidentConfiguration:
  createIncident: false
entityMappings:
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: TargetEmail

Stages and Predicates

Stage 1: source

NordPassEventLogs_CL

Stage 2: where

| where event_type == "integrations"

Stage 3: where

| where action == "activities_token_revoked"

Stage 4: extend

| extend TargetEmail = user_email

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
TargetEmailextend

Oracle - Multiple user agents for single source

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

'Detects requests with different user agents from one source in short timeframe.'

MITRE ATT&CK Coverage

Rule Body

id: 44c7d12a-573f-11ec-bf63-0242ac130002
name: Oracle - Multiple user agents for single source
description: |
  'Detects requests with different user agents from one source in short timeframe.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - OracleWebLogicServer_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let threshold = 5;
  OracleWebLogicServerEvent
  | where isnotempty(HttpUserAgentOriginal)
  | summarize ua_count=makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)
  | where array_length(ua_count) > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5;

Stage 1: source

OracleWebLogicServerEvent

Stage 2: where

| where isnotempty(HttpUserAgentOriginal)

Stage 3: summarize

| summarize ua_count=makeset(HttpUserAgentOriginal) by SrcIpAddr, bin(TimeGenerated, 5m)

Stage 4: where

| where array_length(ua_count) > threshold

Stage 5: 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

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
ua_countsummarize
IPCustomEntityextend

Pathlock TDnR - OData Application Log Events

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

Detects security events from SAP OData service application logs (SLG1), forwarded by Pathlock Threat Detection and Response. OData anomalies may indicate API abuse, unauthorized mass data extraction via OData endpoints, or exploitation of SAP Fiori and S/4HANA OData services.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c59
name: Pathlock TDnR - OData Application Log Events
kind: Scheduled
description: >-
  Detects security events from SAP OData service application logs (SLG1), forwarded by Pathlock
  Threat Detection and Response. OData anomalies may indicate API abuse, unauthorized mass data
  extraction via OData endpoints, or exploitation of SAP Fiori and S/4HANA OData services.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Exfiltration
relevantTechniques:
  - T1213
  - T1048
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SLG1_ODATA"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "SLG1_ODATA"

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

Pathlock TDnR - SAP RFC Gateway Events

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

Detects security-relevant events from the SAP RFC gateway logfile, forwarded by Pathlock Threat Detection and Response. Suspicious gateway activity may indicate unauthorized RFC connections, lateral movement across SAP systems, or command-and-control communication through the gateway.

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement
Command & Control

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c26
name: Pathlock TDnR - SAP RFC Gateway Events
kind: Scheduled
description: >-
  Detects security-relevant events from the SAP RFC gateway logfile, forwarded by Pathlock Threat
  Detection and Response. Suspicious gateway activity may indicate unauthorized RFC connections,
  lateral movement across SAP systems, or command-and-control communication through the gateway.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
  - CommandAndControl
relevantTechniques:
  - T1021
  - T1071
query: |
  Pathlock_TDnR_CL
  | where DataSource == "GATEWAY_LOG"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "GATEWAY_LOG"

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

Potential Build Process Compromise - MDE

#
Status
available
Severity
medium
Time window
5m
Group by
BuildAccount, BuildCommand, BuildParentProcess, BuildProcess, BuildProcessTime, DeviceName, FileEditAccount, FileEditDomain, FileEditUpn, timekey
Source
github.com/Azure/Azure-Sentinel

The query looks for source code files being modified immediately after a build process is started. The purpose of this is to look for malicious code injection during the build process. This query uses Microsoft Defender for Endpoint telemetry. More details: https://techcommunity.microsoft.com/t5/azure-sentinel/monitoring-the-software-supply-chain-with-azure-sentinel/ba-p/2176463

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 1bf6e165-5e32-420e-ab4f-0da8558a8be2
name: Potential Build Process Compromise - MDE
description: |
  'The query looks for source code files being modified immediately after a build process is started. The purpose of this is to look for malicious code injection during the build process. This query uses Microsoft Defender for Endpoint telemetry.
  More details: https://techcommunity.microsoft.com/t5/azure-sentinel/monitoring-the-software-supply-chain-with-azure-sentinel/ba-p/2176463'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
      - DeviceFileEvents
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1554
tags:
  - Solorigate
  - NOBELIUM
query: |
  // How far back to look for events from
  let timeframe = 1d;
  // How close together build events and file modifications should occur to alert (make this smaller to reduce FPs)
  let time_window = 5m;
  // Edit this to include build processes used
  let build_processes = dynamic(["MSBuild.exe", "dotnet.exe", "VBCSCompiler.exe"]);
  // Include any processes that you want to allow to edit files during/around the build process
  let allow_list = dynamic([]);
  DeviceProcessEvents
  | where TimeGenerated > ago(timeframe)
  // Look for build process starts
  | where FileName has_any (build_processes)
  | summarize by BuildParentProcess=InitiatingProcessFileName, BuildProcess=FileName, BuildAccount = AccountName, DeviceName, BuildCommand=ProcessCommandLine, 
  timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated
  | join kind=inner(
  DeviceFileEvents
  | where TimeGenerated > ago(timeframe)
  | where InitiatingProcessFileName !in (allow_list)
  | where ActionType == "FileCreated"  or ActionType == "FileModified"
  // Look for code files, edit this to include file extensions used in build.
  | where FileName endswith ".cs" or FileName endswith ".cpp"
  | summarize by FileEditParentProcess=InitiatingProcessParentFileName, FileEditAccount = InitiatingProcessAccountName, FileEditDomain = InitiatingProcessAccountDomain, FileEditUpn = InitiatingProcessAccountUpn, 
  DeviceName, FileEdited=FileName, FileEditProcess=InitiatingProcessFileName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
  // join where build processes and file modifications seen at same time on same host
  on timekey, DeviceName
  // Limit to only where the file edit happens after the build process starts
  | where BuildProcessTime <= FileEditTime
  | summarize make_set(FileEdited), make_set(FileEditProcess) by timekey, DeviceName, BuildParentProcess, BuildProcess, FileEditAccount, FileEditDomain, FileEditUpn
  | extend HostName = tostring(split(DeviceName, ".")[0]), DomainIndex = toint(indexof(DeviceName, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(DeviceName, DomainIndex + 1), DeviceName)
entityMappings:
  - entityType: Host
    fieldMappings:
    - identifier: HostName
      columnName: HostName
    - identifier: DnsDomain
      columnName: HostNameDomain
  - entityType: Account
    fieldMappings:
    - identifier: FullName
      columnName: FileEditUpn
    - identifier: Name
      columnName: FileEditAccount
    - identifier: UPNSuffix
      columnName: FileEditDomain
version: 1.1.0
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1d;
let time_window = 5m;
let build_processes = dynamic(["MSBuild.exe", "dotnet.exe", "VBCSCompiler.exe"]);
let allow_list = dynamic([]);

Stage 1: source

DeviceProcessEvents

Stage 2: where

| where TimeGenerated > ago(timeframe)

Stage 3: where

| where FileName has_any (build_processes)

Stage 4: summarize

| summarize by BuildParentProcess=InitiatingProcessFileName, BuildProcess=FileName, BuildAccount = AccountName, DeviceName, BuildCommand=ProcessCommandLine, 
timekey= bin(TimeGenerated, time_window), BuildProcessTime=TimeGenerated

Stage 5: join

| join kind=inner(
DeviceFileEvents
| where TimeGenerated > ago(timeframe)
| where InitiatingProcessFileName !in (allow_list)
| where ActionType == "FileCreated"  or ActionType == "FileModified"
| where FileName endswith ".cs" or FileName endswith ".cpp"
| summarize by FileEditParentProcess=InitiatingProcessParentFileName, FileEditAccount = InitiatingProcessAccountName, FileEditDomain = InitiatingProcessAccountDomain, FileEditUpn = InitiatingProcessAccountUpn, 
DeviceName, FileEdited=FileName, FileEditProcess=InitiatingProcessFileName, timekey= bin(TimeGenerated, time_window), FileEditTime=TimeGenerated)
on timekey, DeviceName

Stage 6: where

where BuildProcessTime <= FileEditTime

Stage 7: summarize

summarize by timekey, DeviceName, BuildParentProcess, BuildProcess, FileEditAccount, FileEditDomain, FileEditUpn

Stage 8: extend

extend DomainIndex, HostName

Stage 9: extend

extend HostNameDomain
HostNameDomain =
ifDomainIndex != -1substring(DeviceName, (DomainIndex + 1))
elseDeviceName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • FileCreated
  • FileModified
field:"ActionType" kind:eq
FileNameends_with
  • .cpp
  • .cs
field:"FileName" kind:ends_with
FileNamematch
  • MSBuild.exe transforms: term corpus 2 (kusto 2)
  • VBCSCompiler.exe transforms: term
  • dotnet.exe transforms: term
field:"process_basename" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
InitiatingProcessFileNameeq[]excludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
BuildParentProcesssummarize
BuildProcesssummarize
DeviceNamesummarize
FileEditAccountsummarize
FileEditDomainsummarize
FileEditUpnsummarize
timekeysummarize
DomainIndexextend
HostNameextend
HostNameDomainextend

ProofpointPOD - Multiple archived attachments to the same recipient

#
Status
available
Severity
medium
Time window
30m
Group by
DstUserUpn, SrcUserUpn
Source
github.com/Azure/Azure-Sentinel

'Detects when multiple emails where sent to the same recipient with large archived attachments.'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: bda5a2bd-979b-4828-a91f-27c2a5048f7f
name: ProofpointPOD - Multiple archived attachments to the same recipient
description: |
  'Detects when multiple emails where sent to the same recipient with large archived attachments.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ProofpointPOD
    dataTypes:
      - ProofpointPOD_message_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
relevantTechniques:
  - T1567
query: |
  let lbtime = 30m;
  let msgthreshold = 3;
  let compressedTypes = dynamic(['zip', 'rar', 'tar', 'x-7z-compressed']);
  ProofpointPOD
  | where TimeGenerated > ago(lbtime)
  | where EventType == 'message'
  | where NetworkDirection == 'outbound'
  | extend attachedMimeType = todynamic(MsgParts)[0]['detectedMime']
  | where attachedMimeType has_any (compressedTypes)
  | summarize count(), make_set(attachedMimeType) by SrcUserUpn, DstUserUpn
  | where count_ > msgthreshold
  | extend AccountCustomEntity = SrcUserUpn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let lbtime = 30m;
let msgthreshold = 3;
let compressedTypes = dynamic(['zip', 'rar', 'tar', 'x-7z-compressed']);

Stage 1: source

ProofpointPOD

Stage 2: where

| where TimeGenerated > ago(lbtime)

Stage 3: where

| where EventType == 'message'

Stage 4: where

| where NetworkDirection == 'outbound'

Stage 5: extend

| extend attachedMimeType = todynamic(MsgParts)[0]['detectedMime']

Stage 6: where

| where attachedMimeType has_any (compressedTypes)

Stage 7: summarize

| summarize count(), make_set(attachedMimeType) by SrcUserUpn, DstUserUpn

Stage 8: where

| where count_ > msgthreshold

Stage 9: extend

| extend AccountCustomEntity = SrcUserUpn

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • message
field:"EventType" kind:eq value:"message"
NetworkDirectioneq
  • outbound
field:"NetworkDirection" kind:eq value:"outbound"
attachedMimeTypematch
  • rar transforms: term
  • tar transforms: term
  • x-7z-compressed transforms: term
  • zip transforms: term
field:"attachedMimeType" kind:match
count_gt
  • 3 transforms: number
field:"count_" kind:gt value:"3"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstUserUpnsummarize
SrcUserUpnsummarize
AccountCustomEntityextend

Recovery Token Deleted

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

Detects when a recovery token is deleted. This might indicate unauthorized removal of critical components.

Rule Body

id: 9f37ded4-f27a-4ef6-b5e6-de6430070808
name: Recovery Token Deleted
description: Detects when a recovery token is deleted. This might indicate unauthorized
  removal of critical components.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 36013\n| extend TokenID = extract(\"\
  TokenID=\\\"([^\\\"]*)\\\"\", 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    [\"Token ID\"] = TokenID,\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 == 36013

Stage 3: extend

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

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
Token IDproject
User Nameproject

Semperis DSP RBAC Changes

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

Alerts when there are RBAC changes in the DSP system.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: "e5edf3f3-de53-45e6-b0d7-1ce1c048df4a"
name: Semperis DSP RBAC Changes
description: |
  'Alerts when there are RBAC changes in the DSP system.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SemperisDSP
    dataTypes:
      - dsp_parser
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1548
  - T1098
query: |
  SecurityEvent
  | where EventSourceName == 'Semperis-Operation-Log' and EventID == 20012 
  | order by TimeGenerated desc
  | extend p1Xml = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array p1Xml
  | evaluate bag_unpack(p1Xml)
  | extend Name=column_ifexists('@Name', ''), Value=column_ifexists('#text', '')
  | evaluate pivot(Name, any(Value), TimeGenerated, Computer, Level, EventLevelName, EventID, Type, _ResourceId)
  | extend det = column_ifexists('details', '')
  | parse det with "Occured at (UTC): " OccurredAt "Session ID: " SessionID "Trustee Name: " TrusteeName "Correlation ID: " CorrelationID "Source: " Source "WebSite Target: " WebSiteTarget "Product: " Product "Component: " Component "AD Information: " ADInformation "Object GUID: " ObjectGUID "Attribute: " Attribute "Distinguished Name: " DistinguishedName "Additional Information: "AdditionalInformation "Operation Detail: " OperationDetail "operationName: " operationName "trustee: " trustee "personas: " personas "Status: " status "Granted: " Granted "Result: " Result
  | extend _AccessGranted = iif(operationName contains "CreateRbacIdentity", "Added", "Removed")
  | extend _Identity = iif(operationName contains "CreateRbacIdentity", trustee, tostring(substring(trustee, 1, strlen(trustee))))
  | extend _Identity = iif(operationName contains "CreateRbacIdentity", _Identity, replace_string(_Identity, "'", ""))
  | extend add_personas = replace_string(replace_string(replace_string(personas, "{ Name = ", ""), " }", ""), ";", ",")
  | extend remove_personas = replace_string(personas, ";", ",")
  | extend grid_personas = iif(operationName contains "CreateRbacIdentity", add_personas, remove_personas)
  | extend date_to_sort = format_datetime(TimeGenerated, "yyyy-mm-dd   HH:mm:ss")
  | order by date_to_sort desc
  | extend NTDomain = tostring(split(TrusteeName, '\\', 0)[0]), LoginUser = tostring(split(TrusteeName, '\\', 1)[0])
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: LoginUser
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertDisplayNameFormat: RBAC Change -- Alert from Semperis Directory Services Protector
  alertDescriptionFormat: A RBAC change was detected in the DSP system.
version: 2.0.7
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventSourceName == 'Semperis-Operation-Log' and EventID == 20012

Stage 3: sort

| order by TimeGenerated desc

Stage 4: extend

| extend p1Xml = parse_xml(EventData).DataItem.EventData.Data

Stage 5: mv-expand

| mv-expand bagexpansion=array p1Xml

Stage 6: evaluate

| evaluate bag_unpack(p1Xml)

Stage 7: extend

| extend Name=column_ifexists('@Name', ''), Value=column_ifexists('#text', '')

Stage 8: evaluate

| evaluate pivot(Name, any(Value), TimeGenerated, Computer, Level, EventLevelName, EventID, Type, _ResourceId)

Stage 9: extend

| extend det = column_ifexists('details', '')

Stage 10: parse

| parse det with "Occured at (UTC): " OccurredAt "Session ID: " SessionID "Trustee Name: " TrusteeName "Correlation ID: " CorrelationID "Source: " Source "WebSite Target: " WebSiteTarget "Product: " Product "Component: " Component "AD Information: " ADInformation "Object GUID: " ObjectGUID "Attribute: " Attribute "Distinguished Name: " DistinguishedName "Additional Information: "AdditionalInformation "Operation Detail: " OperationDetail "operationName: " operationName "trustee: " trustee "personas: " personas "Status: " status "Granted: " Granted "Result: " Result

Stage 11: extend (7 consecutive steps)

| extend _AccessGranted = iif(operationName contains "CreateRbacIdentity", "Added", "Removed")
| extend _Identity = iif(operationName contains "CreateRbacIdentity", trustee, tostring(substring(trustee, 1, strlen(trustee))))
| extend _Identity = iif(operationName contains "CreateRbacIdentity", _Identity, replace_string(_Identity, "'", ""))
| extend add_personas = replace_string(replace_string(replace_string(personas, "{ Name = ", ""), " }", ""), ";", ",")
| extend remove_personas = replace_string(personas, ";", ",")
| extend grid_personas = iif(operationName contains "CreateRbacIdentity", add_personas, remove_personas)
| extend date_to_sort = format_datetime(TimeGenerated, "yyyy-mm-dd   HH:mm:ss")

Stage 12: sort

| order by date_to_sort desc

Stage 13: extend

| extend NTDomain = tostring(split(TrusteeName, '\\', 0)[0]), LoginUser = tostring(split(TrusteeName, '\\', 1)[0])

Stage 14: extend

| extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
p1Xmlextend
Nameextend
Valueextend
detextend
_AccessGrantedextend
_Identityextend
add_personasextend
remove_personasextend
grid_personasextend
date_to_sortextend
LoginUserextend
NTDomainextend
DnsDomainextend
HostNameextend

Tailscale: Exit node advertised or approved

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

Identifies when a device starts advertising itself as an exit node, or when an admin approves one. Validate the device and operator - rogue exit nodes can intercept tailnet egress.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: f42f2906-c8e6-23d0-e48c-0620e50d5510
name: "Tailscale: Exit node advertised or approved"
description: |
  Identifies when a device starts advertising itself as an exit node, or when an admin approves one. Validate the device and operator - rogue exit nodes can intercept tailnet egress.
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Exfiltration
relevantTechniques:
  - T1090
query: |
  Tailscale_Audit_CL
        | where Action contains "EXIT" or tostring(New.advertisedExitNode) == "true" or tostring(New.allowedExitNode) == "true"
        | extend ActorLogin = tostring(Actor.loginName)
        | extend NodeName = tostring(Target.name)
        | extend NodeId = tostring(Target.id)
        | project TimeGenerated, ActorLogin, Action, NodeName, NodeId, Origin, New, Old
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: NodeName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action contains "EXIT" or tostring(New.advertisedExitNode) == "true" or tostring(New.allowedExitNode) == "true"

Stage 3: extend (3 consecutive steps)

| extend ActorLogin = tostring(Actor.loginName)
| extend NodeName = tostring(Target.name)
| extend NodeId = tostring(Target.id)

Stage 4: project

| project TimeGenerated, ActorLogin, Action, NodeName, NodeId, Origin, New, Old

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioncontains
  • EXIT
field:"Action" kind:contains value:"EXIT"
New.advertisedExitNodeeq
  • true transforms: tostring, kusto_dynamic_path
field:"New.advertisedExitNode" kind:eq value:"true"
New.allowedExitNodeeq
  • true transforms: tostring, kusto_dynamic_path
field:"New.allowedExitNode" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActorLoginproject
Newproject
NodeIdproject
NodeNameproject
Oldproject
Originproject
TimeGeneratedproject

TI Map IP entity to Azure Key Vault logs

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

Identifies a match in Azure Key Vault logs from any IP IOC from TI

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 7c8051a7-3d29-4c0d-a340-893423f7b0a5
name: TI Map IP entity to Azure Key Vault logs
description: |
  Identifies a match in Azure Key Vault logs from any IP IOC from TI
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: AzureKeyVault
    dataTypes:
      - KeyVaultData
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h; // Look back 1 hour for AzureDiagnostics logs
  let ioc_lookBack = 14d; // Look back 14 days for threat intelligence indicators
  // Fetch threat intelligence indicators related to IP addresses
  let IP_Indicators = ThreatIntelIndicators
  //extract key part of kv pair
       | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
       | where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")
       | extend NetworkSourceIP = toupper(ObservableValue)
       | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
    | where TimeGenerated >= ago(ioc_lookBack)
    | extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
    | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
    | where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil));
  // Perform a join between IP indicators and AzureDiagnostics logs for Key Vault events
  IP_Indicators
     | project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity
    // Use innerunique to keep performance fast and result set low, as we only need one match to indicate potential malicious activity that needs investigation
    | join kind=innerunique (
        AzureDiagnostics
        | where ResourceType =~ "VAULTS"
        | where TimeGenerated >= ago(dt_lookBack)
        | extend KeyVaultEvents_TimeGenerated = TimeGenerated, ClientIP = CallerIPAddress
    )
    on $left.TI_ipEntity == $right.ClientIP
    // Filter out logs that occurred after the expiration of the corresponding indicator
    | where KeyVaultEvents_TimeGenerated < ValidUntil
    // Group the results by IndicatorId and ClientIP, and keep the log entry with the latest timestamp
    | summarize KeyVaultEvents_TimeGenerated = arg_max(KeyVaultEvents_TimeGenerated, *) by Id, ClientIP
    // Select the desired output fields
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project KeyVaultEvents_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence,
      TI_ipEntity, ClientIP, ResourceId, SubscriptionId, OperationName, ResultType, CorrelationId, id_s, clientInfo_s, httpStatusCode_d,
      identity_claim_appid_g, identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, Type
    // Rename the timestamp field
    | extend timestamp = KeyVaultEvents_TimeGenerated
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: ResourceId
version: 1.3.8
kind: Scheduled

Stages and Predicates

Parameters

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

let IP_Indicators is inlined into the numbered stages below.

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

Stage 1: source

ThreatIntelIndicators

Stage 2: extend

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

Stage 3: where

| where IndicatorType in ("ipv4-addr", "ipv6-addr", "network-traffic")

Stage 4: extend

| extend NetworkSourceIP = toupper(ObservableValue)

Stage 5: extend

| extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)

Stage 6: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 7: extend

| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
TI_ipEntity =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: extend

| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
TI_ipEntity =
ifisempty(TI_ipEntity) and isnotempty(NetworkSourceIP)NetworkSourceIP
elseTI_ipEntity

Stage 9: where

| where ipv4_is_private(TI_ipEntity) == false and  TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."

Stage 10: summarize

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

Stage 11: where

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

Stage 12: project-reorder

IP_Indicators
| project-reorder *, Tags, TrafficLightProtocolLevel, NetworkSourceIP, Type, TI_ipEntity

Stage 13: join

| join kind=innerunique (
      AzureDiagnostics
      | where ResourceType =~ "VAULTS"
      | where TimeGenerated >= ago(dt_lookBack)
      | extend KeyVaultEvents_TimeGenerated = TimeGenerated, ClientIP = CallerIPAddress
  )
  on $left.TI_ipEntity == $right.ClientIP

Stage 14: where

where KeyVaultEvents_TimeGenerated < ValidUntil

Stage 15: summarize

summarize KeyVaultEvents_TimeGenerated by Id, ClientIP

Stage 16: extend

extend Description

Stage 17: extend

extend ActivityGroupNames

Stage 18: project

project ActivityGroupNames, ClientIP, Confidence, CorrelationId, Description, Id, KeyVaultEvents_TimeGenerated, OperationName, ResourceId, ResultType, SubscriptionId, TI_ipEntity, Type, ValidUntil, clientInfo_s, httpStatusCode_d, id_s, identity_claim_appid_g, identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g

Stage 19: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
TI_ipEntitycidr_match10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 127.0.0.0/8excludes:TI_ipEntity
TI_ipEntitystarts_with127.excludes:TI_ipEntity field:"TI_ipEntity" value:"127."
TI_ipEntitystarts_with::excludes:TI_ipEntity field:"TI_ipEntity" value:"::"
TI_ipEntitystarts_withfe80excludes:TI_ipEntity field:"TI_ipEntity" value:"fe80"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
ClientIPproject
Confidenceproject
CorrelationIdproject
Descriptionproject
Idproject
KeyVaultEvents_TimeGeneratedproject
OperationNameproject
ResourceIdproject
ResultTypeproject
SubscriptionIdproject
TI_ipEntityproject
Typeproject
ValidUntilproject
clientInfo_sproject
httpStatusCode_dproject
id_sproject
identity_claim_appid_gproject
identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_gproject
timestampextend

UniFi Site Manager: ISP SLA Breach

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

Identifies when ISP uptime falls below the SLA threshold. Useful for tracking SLA compliance and supporting ISP accountability conversations.

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: fecd4ab1-b24e-8413-9164-e3621c8d7caa
name: "UniFi Site Manager: ISP SLA Breach"
description: |
  Identifies when ISP uptime falls below the SLA threshold. Useful for tracking SLA compliance and supporting ISP accountability conversations.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: UniFiSiteManagerConnectorDefinition
    dataTypes:
      - Unifi_SiteManager_ISPMetrics_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1499
subTechniques:
  - T1499.002
query: |
  // UniFi ISP SLA Breach Detection
  let SLAThreshold = 99.9;
  Unifi_SiteManager_ISPMetrics_CL
  | where TimeGenerated > ago(1h)
  | mv-expand period = Periods
  | extend
      metricTime = todatetime(period.metricTime),
      uptime = todouble(period.data.wan.uptime),
      downtime = toint(period.data.wan.downtime),
      ispName = tostring(period.data.wan.ispName),
      ispAsn = tostring(period.data.wan.ispAsn),
      siteIdStr = tostring(SiteId)
  // De-duplicate Periods: each poll returns the same hour buckets, so collapse to latest value per metricTime
  | summarize arg_max(TimeGenerated, uptime, downtime, ispAsn) by siteIdStr, ispName, metricTime
  | where metricTime > ago(1h)
  | summarize
      AvgUptime = round(avg(uptime), 3),
      MinUptime = round(min(uptime), 3),
      TotalDowntimeSeconds = sum(downtime),
      MeasurementCount = count()
      by SiteId = siteIdStr, ispName, ispAsn
  | where AvgUptime < SLAThreshold
  | extend 
      TimeGenerated = now(),
      SLATarget = SLAThreshold,
      UptimeGap = round(SLAThreshold - AvgUptime, 3)
  | project
      TimeGenerated,
      SiteId = SiteId,
      ISPName = ispName,
      ISPAsn = ispAsn,
      AvgUptimePct = AvgUptime,
      MinUptimePct = MinUptime,
      SLATargetPct = SLATarget,
      UptimeGapPct = UptimeGap,
      TotalDowntimeSeconds,
      MeasurementCount
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SiteId
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ISPName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.1

Stages and Predicates

Parameters

let SLAThreshold = 99.9;

Stage 1: source

Unifi_SiteManager_ISPMetrics_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: mv-expand

| mv-expand period = Periods

Stage 4: extend

| extend
    metricTime = todatetime(period.metricTime),
    uptime = todouble(period.data.wan.uptime),
    downtime = toint(period.data.wan.downtime),
    ispName = tostring(period.data.wan.ispName),
    ispAsn = tostring(period.data.wan.ispAsn),
    siteIdStr = tostring(SiteId)

Stage 5: summarize

| summarize arg_max(TimeGenerated, uptime, downtime, ispAsn) by siteIdStr, ispName, metricTime

Stage 6: where

| where metricTime > ago(1h)

Stage 7: summarize

| summarize
    AvgUptime = round(avg(uptime), 3),
    MinUptime = round(min(uptime), 3),
    TotalDowntimeSeconds = sum(downtime),
    MeasurementCount = count()
    by SiteId = siteIdStr, ispName, ispAsn

Stage 8: where

| where AvgUptime < SLAThreshold

Stage 9: extend

| extend 
    TimeGenerated = now(),
    SLATarget = SLAThreshold,
    UptimeGap = round(SLAThreshold - AvgUptime, 3)

Stage 10: project

| project
    TimeGenerated,
    SiteId = SiteId,
    ISPName = ispName,
    ISPAsn = ispAsn,
    AvgUptimePct = AvgUptime,
    MinUptimePct = MinUptime,
    SLATargetPct = SLATarget,
    UptimeGapPct = UptimeGap,
    TotalDowntimeSeconds,
    MeasurementCount

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AvgUptimelt
  • 99.9 transforms: number
field:"AvgUptime" kind:lt value:"99.9"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AvgUptimePctproject
ISPAsnproject
ISPNameproject
MeasurementCountproject
MinUptimePctproject
SLATargetPctproject
SiteIdproject
TimeGeneratedproject
TotalDowntimeSecondsproject
UptimeGapPctproject

Vectra Account's Behaviors

#
Status
available
Severity
informational
Time window
5m
Group by
Activity, source_entity
Source
github.com/Azure/Azure-Sentinel

'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on account's detections.'

MITRE ATT&CK Coverage

Rule Body

id: ce54b5d3-4c31-4eaf-a73e-31412270b6ab
name: Vectra Account's Behaviors
description: |
  'This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. This rule is focused on account's detections.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
  - CredentialAccess
  - Discovery
  - LateralMovement
  - Collection
  - CommandAndControl
  - Exfiltration
  - Impact
relevantTechniques:
  - T1003
  - T1087
  - T1021
  - T1119
  - T1071
  - T1041
  - T1499
query: |
  CommonSecurityLog
  | where DeviceVendor == "Vectra Networks"
  | where DeviceProduct == "X Series"
  | where DeviceEventClassID != "campaigns"
      and DeviceEventClassID != "hsc"
      and DeviceEventClassID != "audit"
      and DeviceEventClassID != "health"
      and DeviceEventClassID != "asc"
  | extend Category = coalesce(
      column_ifexists("DeviceEventCategory", ""), 
      extract("cat=(.+?)(;|$)", 1, AdditionalExtensions), 
      ""
      )
  | project-rename threat_score = FlexNumber1
  | project-rename certainty_score = FlexNumber2
  | project-rename vectra_URL = DeviceCustomString4
  | project-rename detection_name = DeviceEventClassID
  | project-rename triaged = DeviceCustomString5
  | where triaged != "True" and AdditionalExtensions contains "account"
  | extend account = extract("account=(.+?);", 1, AdditionalExtensions)
  | extend upn = iff(account matches regex ":", tostring(split(account, ":")[1]), tostring(split(account, ":")[0]))
  | extend name = tostring(split(upn, "@")[0])
  | extend upn_suffix = tostring(split(upn, "@")[1])
  | extend source_entity = case(isnotempty(upn), upn,
      isnotempty(SourceHostName), SourceHostName,
      "UNKNWON")
  | extend level = case(threat_score == 0 and certainty_score == 0, "Info",
      threat_score < 50 and certainty_score < 50, "Low",
      threat_score < 50 and certainty_score >= 50, "Medium", 
      threat_score >= 50 and certainty_score <= 50, "High", 
      threat_score >= 50 and certainty_score >= 50, "Critical",
      "UNKNOWN")
  | extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)
  | summarize arg_max(threat_score, *) by source_entity, Activity
  | sort by TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: name
      - identifier: UPNSuffix
        columnName: upn_suffix
alertDetailsOverride:
  alertDisplayNameFormat: Vectra AI - {{Activity}} Detected
  alertDescriptionFormat: |
    Entity is an account. Category is {{Category}}. Threat score is {{threat_score}} and certainty score is {{certainty_score}}.
  alertSeverityColumnName: Severity
  alertDynamicProperties:
    - alertProperty: AlertLink
      value: vectra_URL
    - alertProperty: ProductName
      value: DeviceProduct
    - alertProperty: ProviderName
      value: DeviceVendor
    - alertProperty: ConfidenceScore
      value: certainty_score
customDetails:
  AttackType: Activity
  AttackCategory: Category
version: 1.0.5
kind: Scheduled

Stages and Predicates

Stage 1: source

CommonSecurityLog

Stage 2: where

| where DeviceVendor == "Vectra Networks"

Stage 3: where

| where DeviceProduct == "X Series"

Stage 4: where

| where DeviceEventClassID != "campaigns"
    and DeviceEventClassID != "hsc"
    and DeviceEventClassID != "audit"
    and DeviceEventClassID != "health"
    and DeviceEventClassID != "asc"

Stage 5: extend

| extend Category = coalesce(
    column_ifexists("DeviceEventCategory", ""), 
    extract("cat=(.+?)(;|$)", 1, AdditionalExtensions), 
    ""
    )

Stage 6: project-rename

| project-rename threat_score = FlexNumber1

Stage 7: project-rename

| project-rename certainty_score = FlexNumber2

Stage 8: project-rename

| project-rename vectra_URL = DeviceCustomString4

Stage 9: project-rename

| project-rename detection_name = DeviceEventClassID

Stage 10: project-rename

| project-rename triaged = DeviceCustomString5

Stage 11: where

| where triaged != "True" and AdditionalExtensions contains "account"

Stage 12: extend (7 consecutive steps)

| extend account = extract("account=(.+?);", 1, AdditionalExtensions)
| extend upn = iff(account matches regex ":", tostring(split(account, ":")[1]), tostring(split(account, ":")[0]))
| extend name = tostring(split(upn, "@")[0])
| extend upn_suffix = tostring(split(upn, "@")[1])
| extend source_entity = case(isnotempty(upn), upn,
    isnotempty(SourceHostName), SourceHostName,
    "UNKNWON")
| extend level = case(threat_score == 0 and certainty_score == 0, "Info",
    threat_score < 50 and certainty_score < 50, "Low",
    threat_score < 50 and certainty_score >= 50, "Medium", 
    threat_score >= 50 and certainty_score <= 50, "High", 
    threat_score >= 50 and certainty_score >= 50, "Critical",
    "UNKNOWN")
| extend Severity = case(level == "Info", "Informational",level == "Critical", "High", level)

Stage 13: summarize

| summarize arg_max(threat_score, *) by source_entity, Activity

Stage 14: sort

| sort by TimeGenerated

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Activitysummarize
source_entitysummarize

Virtual Lab Deleted

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

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

Rule Body

id: 384e92a7-103c-4a47-945d-381ae9653f91
name: Virtual Lab Deleted
description: Detects when a virtual lab is deleted from Veeam Backup & Replication.
  This might indicate unauthorized removal of critical components.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 30800\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n  MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 30800

Stage 3: project

| project
    Date = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),
    DataSource = original_host,
    EventId = instanceId,
    UserName = user,
  MessageDetails = Description,
    Severity = SeverityDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Back to all Kusto rules