Detection rules › Kusto

Kusto rules: 36

AD account with Don't Expire Password

#
Severity
low
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

Identifies whenever a user account has the setting "Password Never Expires" in the user account properties selected. This is indicated in Security event 4738 in the EventData item labeled UserAccountControl with an included value of %%2089. %%2089 resolves to "Don't Expire Password - Enabled".

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 6c360107-f3ee-4b91-9f43-f4cfd90441cf
name: AD account with Don't Expire Password
description: |
  'Identifies whenever a user account has the setting "Password Never Expires" in the user account properties selected.
  This is indicated in Security event 4738 in the EventData item labeled UserAccountControl with an included value of %%2089.
  %%2089 resolves to "Don't Expire Password - Enabled".'
severity: Low
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
 union isfuzzy=true
 (
  SecurityEvent
  | where EventID == 4738
  // 2089 value indicates the Don't Expire Password value has been set
  | where UserAccountControl has "%%2089"
  | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
  // 2050 indicates that the Password Not Required value is NOT set, this often shows up at the same time as a 2089 and is the recommended value.  This value may not be in the event.
  | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
  // If value %%2082 is present in the 4738 event, this indicates the account has been configured to logon WITHOUT a password. Generally you should only see this value when an account is created and only in Event 4720: Account Creation Event.
  | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
  | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, 
  AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid
  ),
  (
  WindowsEvent
  | where EventID == 4738 and EventData has '2089'
  // 2089 value indicates the Don't Expire Password value has been set
  | extend UserAccountControl = tostring(EventData.UserAccountControl)
  | where UserAccountControl has "%%2089"
  | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
  // 2050 indicates that the Password Not Required value is NOT set, this often shows up at the same time as a 2089 and is the recommended value.  This value may not be in the event.
  | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
  // If value %%2082 is present in the 4738 event, this indicates the account has been configured to logon WITHOUT a password. Generally you should only see this value when an account is created and only in Event 4720: Account Creation Event.
  | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
  | extend Activity="4738 - A user account was changed."
  | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend TargetSid = tostring(EventData.TargetSid)
  | extend SubjectAccount = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
  | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
  | extend AccountType=case(SubjectAccount endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
  | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName), TargetSid, 
  AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName), SubjectUserSid = tostring(EventData.SubjectUserSid)
  )
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | project-away DomainIndex
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetAccount
      - identifier: Name
        columnName: TargetUserName
      - identifier: NTDomain
        columnName: TargetDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: TargetSid
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SubjectAccount
      - identifier: Name
        columnName: SubjectUserName
      - identifier: NTDomain
        columnName: SubjectDomainName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: SubjectUserSid
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.2.2
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Union isfuzzy=true (2 Sources)

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

Leg 1: SecurityEvent

SecurityEvent
 | where EventID == 4738
 | where UserAccountControl has "%%2089"
 | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
 | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
 | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
 | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName, TargetDomainName, TargetSid, 
 AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectUserName, SubjectDomainName, SubjectUserSid

Leg 2: WindowsEvent

WindowsEvent
 | where EventID == 4738 and EventData has '2089'
 | extend UserAccountControl = tostring(EventData.UserAccountControl)
 | where UserAccountControl has "%%2089"
 | extend Value_2089 = iff(UserAccountControl has "%%2089","'Don't Expire Password' - Enabled", "Not Changed")
 | extend Value_2050 = iff(UserAccountControl has "%%2050","'Password Not Required' - Disabled", "Not Changed")
 | extend Value_2082 = iff(UserAccountControl has "%%2082","'Password Not Required' - Enabled", "Not Changed")
 | extend Activity="4738 - A user account was changed."
 | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
 | extend TargetSid = tostring(EventData.TargetSid)
 | extend SubjectAccount = strcat(EventData.SubjectDomainName,"\\", EventData.SubjectUserName)
 | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
 | extend AccountType=case(SubjectAccount endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
 | project StartTime = TimeGenerated, EventID, Activity, Computer, TargetAccount, TargetUserName = tostring(EventData.TargetUserName), TargetDomainName = tostring(EventData.TargetDomainName), TargetSid, 
 AccountType, UserAccountControl, Value_2089, Value_2050, Value_2082, SubjectAccount, SubjectDomainName = tostring(EventData.SubjectDomainName), SubjectUserName = tostring(EventData.SubjectUserName), SubjectUserSid = tostring(EventData.SubjectUserSid)

Applied to the Combined Result

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventDatamatch
  • 2089 transforms: term
field:"EventData" kind:match value:"2089"
EventIDeq
  • 4738 corpus 4 (elastic 3, kusto 1)
field:"EventID" kind:eq value:"4738"
UserAccountControlmatch
  • %%2089 transforms: term
field:"UserAccountControl" kind:match value:"%%2089"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountTypeproject
Activityproject
Computerproject
EventIDproject
StartTimeproject
SubjectAccountproject
SubjectDomainNameproject
SubjectUserNameproject
SubjectUserSidproject
TargetAccountproject
TargetDomainNameproject
TargetSidproject
TargetUserNameproject
UserAccountControlproject
Value_2050project
Value_2082project
Value_2089project
HostNameextend
HostNameDomainextend

AWSCloudTrail - SSM document is publicly exposed

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

Detects an AWS Systems Manager (SSM) document that has been made publicly accessible, which could lead to sensitive information exposure. Verify the document configurations and confirm the change was authorized.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Telemetry Coverage

Rule Body

id: 75647b58-bcc8-4eb5-9658-46698d3fa153
name: AWSCloudTrail - SSM document is publicly exposed
description: |
  Detects an AWS Systems Manager (SSM) document that has been made publicly accessible, which could lead to sensitive information exposure. Verify the document configurations and confirm the change was authorized.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1526
query: |
  AWSCloudTrail
  | where  EventName == "ModifyDocumentPermission" and isempty(ErrorCode) and isempty(ErrorMessage)
  | where todynamic(parse_json(RequestParameters).["accountIdsToAdd"]) == '["all"]'
  | 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]), "")  
  | project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,UserIdentityUserName, SessionMfaAuthenticated, RecipientAccountId, AccountName, AccountUPNSuffix, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, ResponseElements, UserIdentityArn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  EventName: EventName
  AWSRegion: AWSRegion
  UserAgent: UserAgent
  RequestParameters: RequestParameters
alertDetailsOverride:
  alertDisplayNameFormat: SSM document publicly exposed by {{AccountName}} in {{AWSRegion}}
  alertDescriptionFormat: User {{AccountName}} modified SSM document permissions to expose it publicly from {{SourceIpAddress}} in region {{AWSRegion}}.
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

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

Stage 3: where

| where todynamic(parse_json(RequestParameters).["accountIdsToAdd"]) == '["all"]'

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: project

| project TimeGenerated, EventName, EventTypeName, UserIdentityAccountId, UserIdentityPrincipalid, UserAgent,UserIdentityUserName, SessionMfaAuthenticated, RecipientAccountId, AccountName, AccountUPNSuffix, SourceIpAddress, AWSRegion, EventSource, AdditionalEventData, RequestParameters, ResponseElements, UserIdentityArn

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ErrorCodeis_null
  • (no value, null check)
field:"aws::errorCode" kind:is_null
ErrorMessageis_null
  • (no value, null check)
field:"aws::errorMessage" kind:is_null
EventNameeq
  • ModifyDocumentPermission
field:"aws::eventName" kind:eq value:"ModifyDocumentPermission"
RequestParameters.accountIdsToAddeq
  • ["all"] transforms: kusto_accessor_expr:{"expression":"todynamic(parse_json(RequestParameters).accountIdsToAdd)","field":"RequestParameters.accountIdsToAdd"}, kusto_dynamic_path
field:"RequestParameters.accountIdsToAdd" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionproject
AccountNameproject
AccountUPNSuffixproject
AdditionalEventDataproject
EventNameproject
EventSourceproject
EventTypeNameproject
RecipientAccountIdproject
RequestParametersproject
ResponseElementsproject
SessionMfaAuthenticatedproject
SourceIpAddressproject
TimeGeneratedproject
UserAgentproject
UserIdentityAccountIdproject
UserIdentityArnproject
UserIdentityPrincipalidproject
UserIdentityUserNameproject

Certified Pre-Owned - TGTs requested with certificate authentication

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

This query identifies someone using machine certificates to request Kerberos Ticket Granting Tickets (TGTs).

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Telemetry Coverage

Rule Body

id: b838a13c-052e-45b8-a5ac-7d3eb62efa11
name: Certified Pre-Owned - TGTs requested with certificate authentication
description: |
  This query identifies someone using machine certificates to request Kerberos Ticket Granting Tickets (TGTs).
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1036
query: |
  let timeframe=1h;
  SecurityEvent
  | where TimeGenerated >= ago(timeframe)
  | where EventID == 4768
  | project TimeGenerated, Computer, TargetAccount, EventData=parse_xml(EventData)
  | mv-apply d=EventData.EventData.Data on
  (
    where d["@Name"]=="CertIssuerName"
    | project CIN=tostring(d["#text"])
  )
  | where not(isempty(CIN))
  // <DECISION - 1>
  // In some environments, we see a lot of certs starting with a sid and containing live.com. Comment out the next line if you have that as well.
  //| where not(CIN startswith "S-1-")
  // <DECISION - 2>
  // If you're seeing a significant number of machine accounts, it might be due to 802.1X or SCCM. https://twitter.com/MagnusMOD/status/1407800853088591872?s=20.
  // The following line allows you to filter out all endpoints. This does introduce a blindspot, and you need a custom function which provides data about (on-prem) AD machines.
  // Alternatively, you can use DeviceInfo, if you're ingesting that data from MDE.
  | parse CIN with "CN=" MachineName
  //| join kind=leftouter  MyCustomLookupFunction on $left.MachineName == $right.CN
  //| where not(OperatingSystem startswith "Windows 10")

entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetAccount
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1h;

Stage 1: source

SecurityEvent

Stage 2: where

| where TimeGenerated >= ago(timeframe)

Stage 3: where

| where EventID == 4768

Stage 4: project

| project TimeGenerated, Computer, TargetAccount, EventData=parse_xml(EventData)

Stage 5: kusto:mv-apply

| mv-apply d=EventData.EventData.Data on
(
  where d["@Name"]=="CertIssuerName"
  | project CIN=tostring(d["#text"])
)

Stage 6: where

| where not(isempty(CIN))

Stage 7: parse

| parse CIN with "CN=" MachineName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4768 corpus 3 (kusto 2, elastic 1)
field:"EventID" kind:eq value:"4768"
d["@Name"]eq
  • CertIssuerName transforms: kusto_dynamic_path

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
CINis_null(no value, null check)excludes:CIN

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computerproject
EventDataproject
TargetAccountproject
TimeGeneratedproject

Contrast ADR - WAF Alert Correlation

#

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

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

'Correlates Contrast ADR security alerts with WAF logs to identify confirmed attack attempts that were either exploited or blocked. This rule helps security teams prioritize incidents by focusing on attacks that have been validated by application security monitoring.'

MITRE ATT&CK Coverage

Rule Body

id: 93641436-afb3-4921-8828-ceab0d15aaab
name: Contrast ADR - WAF Alert Correlation
description: |
  'Correlates Contrast ADR security alerts with WAF logs to identify confirmed attack attempts that were either exploited or blocked. This rule helps security teams prioritize incidents by focusing on attacks that have been validated by application security monitoring.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ContrastADRCCF
    dataTypes:
      - ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - DefenseEvasion
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1211
  - T1008
query: |
  ContrastADRAttackEvents_CL
  | where result =~ "exploited" or result =~ "blocked"
  | project-rename ip=sourceIp
  //please add you WAF table in place of ContrastWAFLogs_CL and WAF tables source IP or target IP column's inplace of ip and uncomment the queries below
  //| join kind=inner (ContrastWAFLogs_CL | where TimeGenerated >= ago(5m)) on ip
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ip
alertDetailsOverride:
  alertDisplayNameFormat: 'WAF Alert Confirmed {{result}}  by Contrast ADR on {{request_headers_referer}}  endpoint of {{application_name}} '
  alertDescriptionFormat: 'WAF Alert Confirmed {{result}}  by Contrast ADR on {{request_headers_referer}}  endpoint of {{application_name}} '
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: Selected
    groupByEntities:
      - IP
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

ContrastADRAttackEvents_CL

Stage 2: where

| where result =~ "exploited" or result =~ "blocked"

Stage 3: project-rename

| project-rename ip=sourceIp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
resulteq
  • blocked
  • exploited
field:"result" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
ipproject-rename

Cyble Vision Alerts TOR Links

#

CYFIRMA - Attack Surface - Open Ports Medium Rule

#

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

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

"This rule is triggered when CYFIRMA identifies open and publicly accessible ports on internet-facing assets. Exposed services may include SSH, RDP, HTTP, or other potentially sensitive ports, increasing the risk of unauthorized access, exploitation, or reconnaissance by threat actors. Monitoring open ports is critical to reducing the external attack surface and preventing misuse through brute force, service vulnerabilities, or protocol exploitation."

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence
Command & Control

Rule Body

id: 9e18b6c3-d172-4bc6-a7d9-cc7b0a03a69e
name: CYFIRMA - Attack Surface - Open Ports Medium Rule
description: |
  "This rule is triggered when CYFIRMA identifies open and publicly accessible ports on internet-facing assets. Exposed services may include SSH, RDP, HTTP, or other potentially sensitive ports, increasing the risk of unauthorized access, exploitation, or reconnaissance by threat actors. Monitoring open ports is critical to reducing the external attack surface and preventing misuse through brute force, service vulnerabilities, or protocol exploitation."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: CyfirmaAttackSurfaceAlertsConnector
    dataTypes:
      - CyfirmaASOpenPortsAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - CommandAndControl
  - Discovery
  - DefenseEvasion
  - Persistence
relevantTechniques:
  - T1566
  - T1071
  - T1505
query: |
  // Medium Severity - Open Ports Exposure Detected
  let timeFrame = 5m;
  CyfirmaASOpenPortsAlerts_CL
  | where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
  | extend 
      Description=description, 
      FirstSeen=first_seen, 
      LastSeen=last_seen, 
      RiskScore=risk_score, 
      Domain=sub_domain, 
      TopDomain=top_domain, 
      NetworkIP=ip, 
      AlertUID=alert_uid, 
      UID=uid, 
      WebServer=web_server, 
      WebServerVersion=web_server_version, 
      OpenPorts=open_ports, 
      ProviderName='CYFIRMA', 
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      Domain,
      TopDomain,
      RiskScore,
      FirstSeen,
      LastSeen,
      NetworkIP,
      AlertUID,
      UID,
      WebServer,
      WebServerVersion,
      OpenPorts,
      ProductName,
      ProviderName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: TopDomain
      - identifier: DnsDomain
        columnName: Domain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: NetworkIP
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  WebServer: WebServer
  WebServerVersion: WebServerVersion
  OpenPorts: OpenPorts
  Description: Description
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Medium Severity Open Ports Exposure Detected on Assets - Domain: {{Domain}}, IP: {{NetworkIP}}"
  alertDescriptionFormat: "CYFIRMA - Medium Severity Open Ports Exposure Detected on Assets - {{Description}}"
  alertDynamicProperties:
    - alertProperty: ProductName
      value: "DeCYFIR/DeTCT"
    - alertProperty: ProviderName
      value: "CYFIRMA"
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

CyfirmaASOpenPortsAlerts_CL

Stage 2: where

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

Stage 3: extend

| extend 
    Description=description, 
    FirstSeen=first_seen, 
    LastSeen=last_seen, 
    RiskScore=risk_score, 
    Domain=sub_domain, 
    TopDomain=top_domain, 
    NetworkIP=ip, 
    AlertUID=alert_uid, 
    UID=uid, 
    WebServer=web_server, 
    WebServerVersion=web_server_version, 
    OpenPorts=open_ports, 
    ProviderName='CYFIRMA', 
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Description,
    Domain,
    TopDomain,
    RiskScore,
    FirstSeen,
    LastSeen,
    NetworkIP,
    AlertUID,
    UID,
    WebServer,
    WebServerVersion,
    OpenPorts,
    ProductName,
    ProviderName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
Descriptionproject
Domainproject
FirstSeenproject
LastSeenproject
NetworkIPproject
OpenPortsproject
ProductNameproject
ProviderNameproject
RiskScoreproject
TimeGeneratedproject
TopDomainproject
UIDproject
WebServerproject
WebServerVersionproject

CYFIRMA - Brand Intelligence - Executive/People Impersonation Medium Rule

#

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

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

"This rule detects potential impersonation of executive or high-profile individuals across digital platforms such as social media. Such impersonation can be used to mislead stakeholders, perform social engineering attacks, or cause reputational damage to the organization. Timely detection is crucial to assess risk and take down malicious profiles to protect brand and executive identity."

MITRE ATT&CK Coverage

Rule Body

id: 59aa22f2-5b4f-4679-b289-003228255413
name: CYFIRMA - Brand Intelligence - Executive/People Impersonation Medium Rule
description: |
  "This rule detects potential impersonation of executive or high-profile individuals across digital platforms such as social media. 
  Such impersonation can be used to mislead stakeholders, perform social engineering attacks, or cause reputational damage to the organization. 
  Timely detection is crucial to assess risk and take down malicious profiles to protect brand and executive identity."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: CyfirmaBrandIntelligenceAlertsDC
    dataTypes:
      - CyfirmaBIExecutivePeopleAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Reconnaissance
  - ResourceDevelopment
  - InitialAccess
relevantTechniques:
  - T1589.003
  - T1585.001
  - T1566.002
query: |
  // Medium severity - Executive/People Impersonation
  let timeFrame = 5m;
  CyfirmaBIExecutivePeopleAlerts_CL
  | where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=asset_value,
      Impact=impact,
      Recommendation=recommendation,
      PostedDate=posted_date,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      Recommendation,
      PostedDate,
      ProductName,
      ProviderName
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  Description: Description
  TimeGenerated: TimeGenerated
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  Impact: Impact
  Recommendation: Recommendation
  PostedDate: PostedDate
  RiskScore: RiskScore
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Medium Severity Alert - Executive Impersonation - Suspicious Social Media Account Detected - {{AssetValue}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaBIExecutivePeopleAlerts_CL

Stage 2: where

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

Stage 3: extend

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

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

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

#

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

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

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

MITRE ATT&CK Coverage

Rule Body

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

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaIndicators_CL

Stage 2: where

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

Stage 3: extend (6 consecutive steps)

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

Stage 4: mv-expand

| mv-expand extensionKeys

Stage 5: extend (4 consecutive steps)

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

Stage 6: project

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

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

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

DSRM Account Abuse

#
Severity
high
Time window
1h
Group by
Computer, Details, EventID, Image, ProcessId, TargetObject, User, _ResourceId
Author
Vasileios Paschalidis
Source
github.com/Azure/Azure-Sentinel

This query detects an abuse of the DSRM account in order to maintain persistence and access to the organization's Active Directory. Ref: https://adsecurity.org/?p=1785

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rule Body

id: 979c42dd-533e-4ede-b18b-31a84ba8b3d6
name: DSRM Account Abuse
description: | 
  'This query detects an abuse of the DSRM account in order to maintain persistence and access to the organization's Active Directory.
  Ref: https://adsecurity.org/?p=1785'
severity: High
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
query: |
  Event
  | where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
  | parse EventData with * 'ProcessId">' ProcessId "<"* 'Image">' Image "<" * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" * 'User">' User "<" * 
  | where TargetObject has ("HKLM\\System\\CurrentControlSet\\Control\\Lsa\\DsrmAdminLogonBehavior") and Details == "DWORD (0x00000002)"
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ProcessId, Image, TargetObject, Details, _ResourceId
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(User, "\\")[1]), AccountNTDomain = tostring(split(User, "\\")[0])
  | extend ImageFileName = tostring(split(Image, "\\")[-1])
  | extend ImageDirectory = replace_string(Image, ImageFileName, "")
  | project-away DomainIndex
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ProcessId
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: ImageFileName
      - identifier: Directory
        columnName: ImageDirectory
  - entityType: RegistryKey
    fieldMappings:
      - identifier: Key
        columnName: TargetObject
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Vasileios Paschalidis
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Stage 1: source

Event

Stage 2: where

| where EventLog == "Microsoft-Windows-Sysmon/Operational" and EventID in (13)

Stage 3: parse

| parse EventData with * 'ProcessId">' ProcessId "<"* 'Image">' Image "<" * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" * 'User">' User "<" *

Stage 4: where

| where TargetObject has ("HKLM\\System\\CurrentControlSet\\Control\\Lsa\\DsrmAdminLogonBehavior") and Details == "DWORD (0x00000002)"

Stage 5: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, User, ProcessId, Image, TargetObject, Details, _ResourceId

Stage 6: extend (5 consecutive steps)

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

Stage 7: project-away

| project-away DomainIndex

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Detailseq
  • DWORD (0x00000002)
field:"Details" kind:eq value:"DWORD (0x00000002)"
EventIDin
  • 13 corpus 4 (kusto 4)
field:"EventID" kind:in value:"13"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 3 (kusto 3)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
TargetObjectmatch
  • HKLM\System\CurrentControlSet\Control\Lsa\DsrmAdminLogonBehavior transforms: term
field:"TargetObject" kind:match value:"HKLM\System\CurrentControlSet\Control\Lsa\DsrmAdminLogonBehavior"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
Detailssummarize
EndTimesummarize
EventIDsummarize
Imagesummarize
ProcessIdsummarize
StartTimesummarize
TargetObjectsummarize
Usersummarize
_ResourceIdsummarize
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend
ImageFileNameextend
ImageDirectoryextend

GCP Audit Logs - Detect Bulk VM Snapshot Deletion

#
Status
available
Severity
high
Time window
15m
Group by
PrincipalEmail, ProjectId, UserAgent
Source
github.com/Azure/Azure-Sentinel

Detects bulk deletion of Google Cloud VM snapshots within a short time period, which may indicate data destruction or defense evasion activities. VM snapshots are critical for backup and disaster recovery. Bulk deletion of snapshots can prevent recovery from incidents and may indicate malicious activity such as ransomware, data destruction, or an attempt to cover tracks after a security breach. Adversaries may delete snapshots to maximize damage, prevent forensic investigation, or hinder recovery efforts. This rule triggers when multiple snapshots are deleted by the same user within a 1-minute window.

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
GCPcompute.snapshots.delete: delete

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: dfdffdc7-929f-4c7e-8f48-30e5ffddb067
name: GCP Audit Logs - Detect Bulk VM Snapshot Deletion
description: |
  'Detects bulk deletion of Google Cloud VM snapshots within a short time period, which may indicate data destruction or defense evasion activities.
  VM snapshots are critical for backup and disaster recovery. Bulk deletion of snapshots can prevent recovery from incidents and may indicate
  malicious activity such as ransomware, data destruction, or an attempt to cover tracks after a security breach.
  Adversaries may delete snapshots to maximize damage, prevent forensic investigation, or hinder recovery efforts.
  This rule triggers when multiple snapshots are deleted by the same user within a 1-minute window.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: GCPAuditLogsDefinition
    dataTypes:
      - GCPAuditLogs
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
  - DefenseEvasion
relevantTechniques:
  - T1485
  - T1490
  - T1562.001
tags:
  - GCP 
  - Compute
  - Data Destruction
  - Cloud Security
query: |
  // Update these thresholds if noisy in your environment
  let SnapshotDeletionThreshold = 10;
  let TimeWindow = 1m;
  GCPAuditLogs
  | where ServiceName == "compute.googleapis.com"
  | where MethodName has "compute.snapshots.delete"
  | where GCPResourceType == "gce_snapshot" and Severity == "NOTICE"
  | extend 
      AuthzInfoJson = parse_json(AuthorizationInfo),
      RequestMetadataJson = parse_json(RequestMetadata),
      ResponseJson = parse_json(Response)
  | extend PermissionType = tostring(AuthzInfoJson[0].permissionType)
  | where PermissionType == "ADMIN_WRITE"
  | extend 
      CallerIpAddress = tostring(RequestMetadataJson.callerIp),
      UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
      SnapshotName = extract(@"snapshots/([^/]+)", 1, GCPResourceName),
      OperationType = tostring(ResponseJson.operationType),
      OperationId = tostring(ResponseJson.id)
  | summarize 
      SnapshotCount = count(),
      SnapshotList = make_set(SnapshotName, 100),
      FirstDeletion = min(TimeGenerated),
      LastDeletion = max(TimeGenerated),
      OperationIds = make_set(OperationId, 100),
      CallerIPs = make_set(CallerIpAddress, 10)
      by PrincipalEmail, ProjectId, UserAgent
  | where SnapshotCount >= SnapshotDeletionThreshold
  | extend DeletionTimeSpan = LastDeletion - FirstDeletion
  | where DeletionTimeSpan <= TimeWindow
  | extend 
      AccountName = tostring(split(PrincipalEmail, "@")[0]), 
      AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
  | project 
      TimeGenerated = FirstDeletion,
      PrincipalEmail,
      ProjectId,
      SnapshotCount,
      SnapshotList,
      FirstDeletion,
      LastDeletion,
      DeletionTimeSpan,
      CallerIPs,
      UserAgent,
      OperationIds,
      AccountName,
      AccountUPNSuffix
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: PrincipalEmail
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: ProjectId
customDetails:
  ProjectId: ProjectId
  SnapshotCount: SnapshotCount
  SnapshotList: SnapshotList
  FirstDeletion: FirstDeletion
  LastDeletion: LastDeletion
  DeletionTimeSpan: DeletionTimeSpan
  CallerIPs: CallerIPs
  UserAgent: UserAgent
alertDetailsOverride:
  alertDisplayNameFormat: "Bulk VM Snapshot Deletion: {{SnapshotCount}} snapshots deleted by {{PrincipalEmail}} in {{ProjectId}}"
  alertDescriptionFormat: |-
    User {{PrincipalEmail}} deleted {{SnapshotCount}} VM snapshots in project {{ProjectId}} within a short time period. 
    This may indicate ransomware, data destruction, or defense evasion activity. Verify authorization, check remaining snapshots, review IP addresses, and investigate for compromise.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let SnapshotDeletionThreshold = 10;
let TimeWindow = 1m;

Stage 1: source

GCPAuditLogs

Stage 2: where

| where ServiceName == "compute.googleapis.com"

Stage 3: where

| where MethodName has "compute.snapshots.delete"

Stage 4: where

| where GCPResourceType == "gce_snapshot" and Severity == "NOTICE"

Stage 5: extend

| extend 
    AuthzInfoJson = parse_json(AuthorizationInfo),
    RequestMetadataJson = parse_json(RequestMetadata),
    ResponseJson = parse_json(Response)

Stage 6: extend

| extend PermissionType = tostring(AuthzInfoJson[0].permissionType)

Stage 7: where

| where PermissionType == "ADMIN_WRITE"

Stage 8: extend

| extend 
    CallerIpAddress = tostring(RequestMetadataJson.callerIp),
    UserAgent = tostring(RequestMetadataJson.callerSuppliedUserAgent),
    SnapshotName = extract(@"snapshots/([^/]+)", 1, GCPResourceName),
    OperationType = tostring(ResponseJson.operationType),
    OperationId = tostring(ResponseJson.id)

Stage 9: summarize

| summarize 
    SnapshotCount = count(),
    SnapshotList = make_set(SnapshotName, 100),
    FirstDeletion = min(TimeGenerated),
    LastDeletion = max(TimeGenerated),
    OperationIds = make_set(OperationId, 100),
    CallerIPs = make_set(CallerIpAddress, 10)
    by PrincipalEmail, ProjectId, UserAgent

Stage 10: where

| where SnapshotCount >= SnapshotDeletionThreshold

Stage 11: extend

| extend DeletionTimeSpan = LastDeletion - FirstDeletion

Stage 12: where

| where DeletionTimeSpan <= TimeWindow

Stage 13: extend

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

Stage 14: project

| project 
    TimeGenerated = FirstDeletion,
    PrincipalEmail,
    ProjectId,
    SnapshotCount,
    SnapshotList,
    FirstDeletion,
    LastDeletion,
    DeletionTimeSpan,
    CallerIPs,
    UserAgent,
    OperationIds,
    AccountName,
    AccountUPNSuffix

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
AccountUPNSuffixproject
CallerIPsproject
DeletionTimeSpanproject
FirstDeletionproject
LastDeletionproject
OperationIdsproject
PrincipalEmailproject
ProjectIdproject
SnapshotCountproject
SnapshotListproject
TimeGeneratedproject
UserAgentproject

Multi-Factor Authentication Disabled

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

Detects when multi-factor authentication is disabled for all users.

Rule Body

id: 6881f4f5-f58e-43d7-a05e-b5d05a4c09f9
name: Multi-Factor Authentication Disabled
description: Detects when multi-factor authentication is disabled for all users.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 40201\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = 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 == 40201

Stage 3: project

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

Multiple Password Reset by user

#
Severity
low
Time window
1d
Group by
Account, Type
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query will determine multiple password resets by user across multiple data sources. Account manipulation including password reset may aid adversaries in maintaining access to credentials and certain permission levels within an environment.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id:  0b9ae89d-8cad-461c-808f-0494f70ad5c4
name: Multiple Password Reset by user
description: |
  'This query will determine multiple password resets by user across multiple data sources.
  Account manipulation including password reset may aid adversaries in maintaining access to credentials and certain permission levels within an environment.'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
     - AuditLogs
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: Syslog
    dataTypes:
      - Syslog
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110
query: |
  let selfServicePasswordReset = dynamic(["Self-service password reset flow activity progress", "Change password (self-service)", "Reset password (self-service)"]); 
  //Self-service password reset flow activity progress is typically caused by a password policy which requires users to rotate passwords. This operation already implies the user has signed in successfully and therefore the password reset is non-malicious.
  let PerUserThreshold = 5;
  let TotalThreshold = 100;
  let action = dynamic(["change", "changed", "reset"]);
  let pWord = dynamic(["password", "credentials"]);
  let PasswordResetMultiDataSource =
  (union isfuzzy=true
  (//Password reset events
  //4723: An attempt was made to change an account's password
  //4724: An attempt was made to reset an accounts password
  SecurityEvent
  | where EventID in ("4723","4724")
  | project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
  (//Password reset events
  //4723: An attempt was made to change an account's password
  //4724: An attempt was made to reset an accounts password
  WindowsEvent
  | where EventID in ("4723","4724")
  | extend SubjectUserSid = tostring(EventData.SubjectUserSid)
  | extend TargetUserName = tostring(EventData.TargetUserName)
  | extend Account = strcat(tostring(EventData.SubjectDomainName),"\\", tostring(EventData.SubjectUserName))
  | extend AccountType=case(Account endswith "$" or SubjectUserSid in ("S-1-5-18", "S-1-5-19", "S-1-5-20"), "Machine", isempty(SubjectUserSid), "", "User")
  | project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName),
  (//Azure Active Directory Password reset events
  AuditLogs
  | where OperationName has_any (pWord) and OperationName has_any (action) and Result =~ "success"
  | where OperationName !in (selfServicePasswordReset)
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend AccountType = tostring(TargetResource.type),
                 Account = tostring(InitiatedBy.user.userPrincipalName),
                 TargetUserName = tolower(tostring(TargetResource.userPrincipalName))
    )
  | project TimeGenerated, AccountType, Account, TargetUserName, Computer = "", Type),
  (//OfficeActive ActiveDirectory Password reset events
  OfficeActivity
  | where OfficeWorkload == "AzureActiveDirectory"
  | where (ExtendedProperties has_any (pWord) or ModifiedProperties has_any (pWord)) and (ExtendedProperties has_any (action) or ModifiedProperties has_any (action))
  | extend AccountType = UserType, Account = OfficeObjectId
  | project TimeGenerated, AccountType, Account, Type, Computer = ""),
  (// Unix syslog password reset events
  Syslog
  | where Facility in ("auth","authpriv")
  | where SyslogMessage has_any (pWord) and SyslogMessage has_any (action)
  | extend AccountType = iif(SyslogMessage contains "root", "Root", "Non-Root")
  | where SyslogMessage matches regex ".*password changed for.*"
  | parse SyslogMessage with * "password changed for" Account
  | project TimeGenerated, AccountType, Account, Computer = HostName, Type)
  );
  let pwrmd = PasswordResetMultiDataSource
  | project TimeGenerated, Computer, AccountType, Account, Type, TargetUserName;
  (union isfuzzy=true
  (pwrmd
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Computerlist = make_set(Computer, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Account, Type
  | where Total > PerUserThreshold
  | extend ResetPivot = "PerUserReset"),
  (pwrmd
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ComputerList = make_set(Computer, 25), AccountList = make_set(Account, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Type
  | where Total > TotalThreshold
  | extend ResetPivot = "TotalUserReset")
  )
  | extend timestamp = StartTimeUtc, HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), Name = tostring(split(Account, '@', 0)[0]), UPNSuffix = tostring(split(Account, '@', 1)[0]), TargetName = tostring(split(TargetUserName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUserName
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
version: 2.1.7
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let selfServicePasswordReset = dynamic(["Self-service password reset flow activity progress", "Change password (self-service)", "Reset password (self-service)"]);
let PerUserThreshold = 5;
let TotalThreshold = 100;
let action = dynamic(["change", "changed", "reset"]);
let pWord = dynamic(["password", "credentials"]);

Union isfuzzy=true (2 Sources)

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

Leg 1: pwrmd

pwrmd
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), Computerlist = make_set(Computer, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Account, Type
| where Total > PerUserThreshold
| extend ResetPivot = "PerUserReset"

Leg 2: pwrmd

pwrmd
| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated), ComputerList = make_set(Computer, 25), AccountList = make_set(Account, 25), AccountType = make_set(AccountType, 25), Computer = arg_max(Computer , TimeGenerated), TargetUserList = make_set(TargetUserName, 25), TargetUserName = arg_max(TargetUserName, TimeGenerated), Total=count() by Type
| where Total > TotalThreshold
| extend ResetPivot = "TotalUserReset"

Applied to the Combined Result

| extend timestamp = StartTimeUtc, HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.')), Name = tostring(split(Account, '@', 0)[0]), UPNSuffix = tostring(split(Account, '@', 1)[0]), TargetName = tostring(split(TargetUserName,'@',0)[0]), TargetUPNSuffix = tostring(split(TargetUserName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDin
  • 4723
  • 4724
field:"EventID" kind:in
ExtendedPropertiesmatch
  • change transforms: term
  • changed transforms: term
  • credentials transforms: term
  • password transforms: term
  • reset transforms: term
field:"ExtendedProperties" kind:match
Facilityin
  • auth
  • authpriv
field:"Facility" kind:in
ModifiedPropertiesmatch
  • change transforms: term
  • changed transforms: term
  • credentials transforms: term
  • password transforms: term
  • reset transforms: term
field:"ModifiedProperties" kind:match
OfficeWorkloadeq
  • AzureActiveDirectory
  • AzureActiveDirectory
field:"m365::Workload" kind:eq
OperationNamematch
  • change transforms: term
  • changed transforms: term
  • credentials transforms: term
  • password transforms: term
  • reset transforms: term
field:"OperationName" kind:match
Resulteq
  • success
field:"Result" kind:eq value:"success"
SyslogMessagematch
  • change transforms: term
  • changed transforms: term
  • credentials transforms: term
  • password transforms: term
  • reset transforms: term
field:"SyslogMessage" kind:match
SyslogMessageregex_match
  • .*password changed for.*
field:"SyslogMessage" kind:regex_match value:".*password changed for.*"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"
Totalgt
  • 100 transforms: number
  • 5 transforms: number
field:"Total" kind:gt

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
OperationNameinChange password (self-service), Reset password (self-service), Self-service password reset flow activity progressexcludes:OperationName field:"OperationName" value:"Change password (self-service)" field:"OperationName" value:"Reset password (self-service)" field:"OperationName" value:"Self-service password reset flow activity progress"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountListsummarize
AccountTypesummarize
Computersummarize
ComputerListsummarize
EndTimeUtcsummarize
StartTimeUtcsummarize
TargetUserListsummarize
TargetUserNamesummarize
Totalsummarize
Typesummarize
ResetPivotextend
DnsDomainextend
HostNameextend
Nameextend
TargetNameextend
TargetUPNSuffixextend
UPNSuffixextend
timestampextend

NGINX - Core Dump

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

'Detects a core dump of a crashing Nginx worker process, which could be a signal of a serious problem or exploitation attempts.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 9a7f5a97-354b-4eac-b407-a1cc7fc4b4ec
name: NGINX - Core Dump
description: |
  'Detects a core dump of a crashing Nginx worker process, which could be a signal of a serious problem or exploitation attempts.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - NGINX_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1499
query: |
  NGINXHTTPServer
  | where EventType =~ "ErrorLog"
  | where EventMessage contains 'exited on signal 6 (core dumped)'
  | extend ProcessIdCustomEntity = ProcessId
entityMappings:
  - entityType: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ProcessIdCustomEntity
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

NGINXHTTPServer

Stage 2: where

| where EventType =~ "ErrorLog"

Stage 3: where

| where EventMessage contains 'exited on signal 6 (core dumped)'

Stage 4: extend

| extend ProcessIdCustomEntity = ProcessId

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ProcessIdCustomEntityextend

Pathlock TDnR - SAP Client Configuration Changes

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

Detects changes to SAP client configuration settings, forwarded by Pathlock Threat Detection and Response. Changes to client settings such as enabling client-independent changes or transport settings may indicate attempts to bypass change management controls.

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Defense Impairment

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c20
name: Pathlock TDnR - SAP Client Configuration Changes
kind: Scheduled
description: >-
  Detects changes to SAP client configuration settings, forwarded by Pathlock Threat Detection
  and Response. Changes to client settings such as enabling client-independent changes or transport
  settings may indicate attempts to bypass change management controls.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Persistence
relevantTechniques:
  - T1562
  - T1098
query: |
  Pathlock_TDnR_CL
  | where DataSource == "CLIENT_CHANGES"
  | 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 == "CLIENT_CHANGES"

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

Spearphishing Attachment: ISO Images (Microsoft Defender for Endpoint)

#
Author
Cyb3rMonk
Source
github.com/Cyb3r-Monk/Threat-Hunting-and-Detection

ISO images are often meant to be used offline and they are often used by IT Admins and/or used on Servers.
Installation from an iso file don't require network connection most of the time. Activities deviating from these situations can be considered as highly suspicious. Below queries detects opening a mounted image, process creation under a mounted image, and network connection from a process created under a mounted image.
All detections can be used seperately or combined together to generate a higher fidelity alert. Detect opening of a mounted image:

MITRE ATT&CK Coverage

TacticTechniques
Initial AccessNo specific technique

Telemetry Coverage

Rule Body

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-1-c4f953edd13f?source=friends_link&sk=e685d7d44928edd142972a4041463f10
// Part-2: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-2-f8dd600430e2?source=friends_link&sk=38b7cd310a4929c25d3eefc545683d5f
//
//
// Query parameters:
let lookback = 1h;
// Get ISO mount events
DeviceFileEvents
| where Timestamp > ago(lookback)
| where FileName endswith ".iso.lnk" or FileName endswith ".img.lnk"
// Exclude servers and workstation used by IT admins if needed.

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-1-c4f953edd13f?source=friends_link&sk=e685d7d44928edd142972a4041463f10
// Part-2: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-2-f8dd600430e2?source=friends_link&sk=38b7cd310a4929c25d3eefc545683d5f
//
//
// Query parameters:
let lookback = 1h;
// Get mounted devices and extract the folder name
DeviceRegistryEvents
| where Timestamp > ago(lookback)
| where ActionType == "RegistryValueSet" and RegistryKey == @"HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices" and RegistryValueName startswith @"\DosDevices\"
| extend Folder = toupper(replace(@'\\DosDevices\\(\w:)',@'\1',RegistryValueName)) // Extract the folder name
// Get process creations that have the mounted image as the FolderPath
| join kind=inner 
    (
    DeviceProcessEvents
    | where Timestamp > ago(lookback)
    | extend Folder = toupper(replace(@'(\w:)\\.*',@'\1',FolderPath))
    ) on DeviceId, Folder
// If needed, exclude servers from the results.

// Author: Cyb3rMonk(https://twitter.com/Cyb3rMonk, https://mergene.medium.com)
// Link to original post:
// Part-1: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-1-c4f953edd13f?source=friends_link&sk=e685d7d44928edd142972a4041463f10
// Part-2: https://mergene.medium.com/detecting-initial-access-html-smuggling-and-iso-images-part-2-f8dd600430e2?source=friends_link&sk=38b7cd310a4929c25d3eefc545683d5f
//
//
// Query parameters:
let lookback = 1h;
// Get mounted devices and extract the folder name
DeviceRegistryEvents
| where Timestamp > ago(lookback)
| where ActionType == "RegistryValueSet" and RegistryKey == @"HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices" and RegistryValueName startswith @"\DosDevices\"
| extend Folder = toupper(replace(@'\\DosDevices\\(\w:)',@'\1',RegistryValueName)) // Extract the folder name
// Get network connections of processes that have the mounted image as the InitiatingProcessFolderPath
| join kind=inner 
    (
    DeviceNetworkEvents
    | where Timestamp > ago(lookback)
    | extend Folder = toupper(replace(@'(\w:)\\.*',@'\1',InitiatingProcessFolderPath))
    ) on DeviceId, Folder
// If needed, exclude the legitimate activity and servers

Stages and Predicates

Parameters

let lookback = 1h;

Stage 1: source

DeviceFileEvents

Stage 2: where

where Timestamp > ago(3600s)

Stage 3: where

where (FileName endswith ".img.lnk" or FileName endswith ".iso.lnk")

Stage 4: source

DeviceRegistryEvents

Stage 5: where

where Timestamp > ago(3600s)

Stage 6: where

where ActionType == "RegistryValueSet" and RegistryKey == @"HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices" and RegistryValueName startswith @"\DosDevices\"

Stage 7: extend

extend Folder

Stage 8: join

join kind=inner (DeviceProcessEvents) on DeviceId, Folder

Stage 9: source

DeviceRegistryEvents

Stage 10: where

where Timestamp > ago(3600s)

Stage 11: where

where ActionType == "RegistryValueSet" and RegistryKey == @"HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices" and RegistryValueName startswith @"\DosDevices\"

Stage 12: extend

extend Folder

Stage 13: join

join kind=inner (DeviceNetworkEvents) on DeviceId, Folder

Indicators

These rows show field, operator, and value matches.

TI Map Domain entity to Cloud App Events

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

Identifies compromises and attacks and detect malicious activities in one's domain entity from TI.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: a7d2b1e4-dd9c-40fd-9651-1a136eb8f0df
name: TI Map Domain entity to Cloud App Events
description: |
    'Identifies compromises and attacks and detect malicious activities in one's domain entity from TI.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - CloudAppEvents
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d; 
  let list_tlds =
    ThreatIntelIndicators
      | where TimeGenerated >= ago(ioc_lookBack)
      | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
      | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
      //extract key part of kv pair
      | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
      | where IndicatorType == "domain-name"
      | extend DomainName = tolower(IndicatorType)
      | extend parts = split(DomainName, '.')
      | extend tld = parts[(array_length(parts) - 1)]
      | extend IndicatorId = tostring(split(Id, "--")[2])
      | summarize count() by tostring(tld)
      | summarize make_set(tld);
  let Domain_Indicators =
    ThreatIntelIndicators
      | where TimeGenerated >= ago(ioc_lookBack)
      | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
      | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
      | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
      | where IndicatorType == "domain-name"
      | extend DomainName = tolower(ObservableValue)
      | extend IndicatorId = tostring(split(Id, "--")[2])
      | project-reorder *, IndicatorType, DomainName, Type;
    Domain_Indicators
      | join kind=innerunique (
    CloudAppEvents
      | extend IngestionTime = ingestion_time()
      | where IngestionTime > ago(dt_lookBack)
      | extend PAUrl = columnifexists("RequestURL", "None")
      | extend Domain = tolower(trim('"', tostring(parseurl(PAUrl).Host)))
      | extend parts = split(Domain, '.')
      | extend tld = parts[(array_length(parts) - 1)]
      | extend CloudAppEvents_TimeGenerated = TimeGenerated) on $left.DomainName == $right.Domain
      | where CloudAppEvents_TimeGenerated < ValidUntil
      | summarize CloudAppEvents_TimeGenerated = argmax(CloudAppEvents_TimeGenerated, *) by IndicatorId
      | extend 
            Description = tostring(parse_json(max_CloudAppEvents_TimeGenerated_Data).description),
            ActivityGroupNames = column_ifexists("max_CloudAppEvents_TimeGenerated_ActivityGroupNames", ""),
            ThreatType = column_ifexists("max_CloudAppEvents_TimeGenerated_ThreatType", ""),
            ExpirationDateTime = column_ifexists("max_CloudAppEvents_TimeGenerated_ExpirationDateTime", ""),
            ConfidenceScore = column_ifexists("max_CloudAppEvents_TimeGenerated_ConfidenceScore", ""),
            DomainName = column_ifexists("max_CloudAppEvents_TimeGenerated_DomainName", ""),
            ProviderName = column_ifexists("max_CloudAppEvents_TimeGenerated_IndicatorProvider", ""),
            AlertSeverity = column_ifexists("max_CloudAppEvents_TimeGenerated_ThreatSeverity", ""),
            IPAddress = column_ifexists("max_CloudAppEvents_TimeGenerated_IPAddress", "")
      | project CloudAppEvents_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, DomainName, ProviderName, AlertSeverity, IPAddress
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.0.7
kind: Scheduled

Stages and Predicates

Parameters

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

let Domain_Indicators is inlined into the numbered stages below.

Let Binding: list_tlds

let list_tlds = ThreatIntelIndicators
    | where TimeGenerated >= ago(ioc_lookBack)
    | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
    | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
    | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
    | where IndicatorType == "domain-name"
    | extend DomainName = tolower(IndicatorType)
    | extend parts = split(DomainName, '.')
    | extend tld = parts[(array_length(parts) - 1)]
    | extend IndicatorId = tostring(split(Id, "--")[2])
    | summarize count() by tostring(tld)
    | summarize make_set(tld);

Stages 1 to 9 define let Domain_Indicators as the rule's main pipeline source. Stages 10 to 14 use it.

Stage 1: source

ThreatIntelIndicators

Stage 2: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 3: summarize

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

Stage 4: where

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

Stage 5: extend

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

Stage 6: where

| where IndicatorType == "domain-name"

Stage 7: extend

| extend DomainName = tolower(ObservableValue)

Stage 8: extend

| extend IndicatorId = tostring(split(Id, "--")[2])

Stage 9: project-reorder

| project-reorder *, IndicatorType, DomainName, Type

Stage 10: join

Domain_Indicators
| join kind=innerunique (
  CloudAppEvents
    | extend IngestionTime = ingestion_time()
    | where IngestionTime > ago(dt_lookBack)
    | extend PAUrl = columnifexists("RequestURL", "None")
    | extend Domain = tolower(trim('"', tostring(parseurl(PAUrl).Host)))
    | extend parts = split(Domain, '.')
    | extend tld = parts[(array_length(parts) - 1)]
    | extend CloudAppEvents_TimeGenerated = TimeGenerated) on $left.DomainName == $right.Domain

Stage 11: where

where CloudAppEvents_TimeGenerated < ValidUntil

Stage 12: summarize

summarize CloudAppEvents_TimeGenerated by IndicatorId

Stage 13: extend

extend ActivityGroupNames, AlertSeverity, ConfidenceScore, Description, DomainName, ExpirationDateTime, IPAddress, ProviderName, ThreatType

Stage 14: project

project ActivityGroupNames, AlertSeverity, CloudAppEvents_TimeGenerated, ConfidenceScore, Description, DomainName, ExpirationDateTime, IPAddress, IndicatorId, ProviderName, ThreatType

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AlertSeverityproject
CloudAppEvents_TimeGeneratedproject
ConfidenceScoreproject
Descriptionproject
DomainNameproject
ExpirationDateTimeproject
IPAddressproject
IndicatorIdproject
ProviderNameproject
ThreatTypeproject

TI Map IP Entity to DnsEvents

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

This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in DnsEvents.

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 6418fd33-92f2-407b-bd61-91c0d4bbcb8a
name: TI Map IP Entity to DnsEvents
description: |
  This query maps any IP indicators of compromise (IOCs) from threat intelligence (TI), by searching for matches in DnsEvents.
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: DNS
    dataTypes:
      - DnsEvents
  - 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 DNS events
  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)
    | extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
    | 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 DNS 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 (
        DnsEvents
        | where TimeGenerated >= ago(dt_lookBack)
        | where SubType =~ "LookupQuery" and isnotempty(IPAddresses)
        | mv-expand SingleIP = split(IPAddresses, ", ") to typeof(string)
        | extend DNS_TimeGenerated = TimeGenerated
    )
    on $left.TI_ipEntity == $right.SingleIP
    // Filter out DNS events that occurred after the expiration of the corresponding indicator
    | where DNS_TimeGenerated < ValidUntil
    // Group the results by IndicatorId and SingleIP, and keep the DNS event with the latest timestamp
    | summarize DNS_TimeGenerated = arg_max(DNS_TimeGenerated, *) by Id, SingleIP
    // Select the desired output fields
    | extend Description = tostring(parse_json(Data).description)
    | extend ActivityGroupNames = extract(@"ActivityGroup:(\S+)", 1, tostring(parse_json(Data).labels))
    | project DNS_TimeGenerated, Description, ActivityGroupNames, Id, ValidUntil, Confidence, TI_ipEntity, Computer, EventId, SubType, ClientIP, Name, IPAddresses, NetworkSourceIP, Type, Url
    | extend timestamp = DNS_TimeGenerated, HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.4.7
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 10 define let IP_Indicators as the rule's main pipeline source. Stages 11 to 18 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 (3 consecutive steps)

| extend TI_ipEntity = iff(isnotempty(NetworkSourceIP), NetworkSourceIP, NetworkSourceIP)
| extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity)
| extend Url = iff(ObservableKey == "url:value", ObservableValue, "")
TI_ipEntity =
ifisnotempty(NetworkSourceIP)NetworkSourceIP
elseNetworkSourceIP

Stage 8: where

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

Stage 9: summarize

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

Stage 10: where

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

Stage 11: project-reorder

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

Stage 12: join

| join kind=innerunique (
      DnsEvents
      | where TimeGenerated >= ago(dt_lookBack)
      | where SubType =~ "LookupQuery" and isnotempty(IPAddresses)
      | mv-expand SingleIP = split(IPAddresses, ", ") to typeof(string)
      | extend DNS_TimeGenerated = TimeGenerated
  )
  on $left.TI_ipEntity == $right.SingleIP

Stage 13: where

where DNS_TimeGenerated < ValidUntil

Stage 14: summarize

summarize DNS_TimeGenerated by Id, SingleIP

Stage 15: extend

extend Description

Stage 16: extend

extend ActivityGroupNames

Stage 17: project

project ActivityGroupNames, ClientIP, Computer, Confidence, DNS_TimeGenerated, Description, EventId, IPAddresses, Id, Name, NetworkSourceIP, SubType, TI_ipEntity, Type, Url, ValidUntil

Stage 18: extend

extend DnsDomain, HostName, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
IPAddressesis_not_null
  • (no value, null check)
field:"IPAddresses" kind:is_not_null
IndicatorTypein
  • ipv4-addr
  • ipv6-addr
  • network-traffic
field:"IndicatorType" kind:in
IsActiveeq
  • true transforms: boolean
field:"IsActive" kind:eq value:"true"
SubTypeeq
  • LookupQuery
field:"SubType" kind:eq value:"LookupQuery"
ValidUntilis_null
  • (no value, null check)
field:"ValidUntil" kind:is_null

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
Computerproject
Confidenceproject
DNS_TimeGeneratedproject
Descriptionproject
EventIdproject
IPAddressesproject
Idproject
Nameproject
NetworkSourceIPproject
SubTypeproject
TI_ipEntityproject
Typeproject
Urlproject
ValidUntilproject
DnsDomainextend
HostNameextend
timestampextend

Uniqkey - Security policy change

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

Detects modifications to Uniqkey organization security policies, such as authentication, password or sharing policy settings. Weakening a policy is a common preparatory step before credential abuse, and even legitimate changes deserve a review trail, so each policy change is raised as an alert with the acting administrator attached.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: ac008972-e70f-4040-96ad-bdce1c642839
name: Uniqkey - Security policy change
version: 1.0.0
kind: Scheduled
description: |-
  Detects modifications to Uniqkey organization security policies, such as authentication, password or sharing policy settings. Weakening a policy is a common preparatory step before credential abuse, and even legitimate changes deserve a review trail, so each policy change is raised as an alert with the acting administrator attached.
severity: Medium
requiredDataConnectors:
  - connectorId: UniqkeyEventsConnector
    dataTypes:
      - UniqkeyEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- Persistence
relevantTechniques:
- T1562
query: |-
  UniqkeyEvents_CL
  | where Category == "policy_management"
  | project TimeGenerated, ActorEmail, ActorType, Action, ActionId, TargetType, TargetName, ClientSystem, SrcIpAddr
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorEmail
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Stage 1: source

UniqkeyEvents_CL

Stage 2: where

| where Category == "policy_management"

Stage 3: project

| project TimeGenerated, ActorEmail, ActorType, Action, ActionId, TargetType, TargetName, ClientSystem, SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • policy_management
field:"Category" kind:eq value:"policy_management"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
ActionIdproject
ActorEmailproject
ActorTypeproject
ClientSystemproject
SrcIpAddrproject
TargetNameproject
TargetTypeproject
TimeGeneratedproject

Users searching for VIP user activity

#
Severity
low
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query monitors for users running Log Analytics queries that contain filters for specific, defined VIP user accounts or the VIPUser watchlist template. Use this detection to alert for users specifically searching for activity of sensitive users.

MITRE ATT&CK Coverage

Rule Body

id: f7f4a77e-f68f-4b56-9aaf-a0c9d87d7a8e
name: Users searching for VIP user activity
description: |
    This query monitors for users running Log Analytics queries that contain filters for specific, defined VIP user accounts or the VIPUser watchlist template.
    Use this detection to alert for users specifically searching for activity of sensitive users.
severity: Low
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Exfiltration
relevantTechniques:
  - T1530
  - T1213
  - T1020
query: |
  // Replace these with the username or emails of your VIP users you wish to monitor for.
  let vips = dynamic(['vip1@email.com','vip2@email.com']);
  // Add users who are allowed to conduct these searches - this could be specific SOC team members
  let allowed_users = dynamic([]);
  LAQueryLogs
  | where QueryText has_any (vips) or QueryText has_any ('_GetWatchlist("VIPUsers")', "_GetWatchlist('VIPUsers')")
  | where AADEmail !in (allowed_users)
  | project TimeGenerated, AADEmail, RequestClientApp, QueryText, ResponseRowCount, RequestTarget
  | extend timestamp = TimeGenerated, AccountName = tostring(split(AADEmail, "@")[0]), AccountUPNSuffix = tostring(split(AADEmail, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: AzureResource
    fieldMappings:
      - identifier: ResourceId
        columnName: RequestTarget
version: 1.1.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let vips = dynamic(['vip1@email.com','vip2@email.com']);
let allowed_users = dynamic([]);

Stage 1: source

LAQueryLogs

Stage 2: where

| where QueryText has_any (vips) or QueryText has_any ('_GetWatchlist("VIPUsers")', "_GetWatchlist('VIPUsers')")

Stage 3: where

| where AADEmail !in (allowed_users)

Stage 4: project

| project TimeGenerated, AADEmail, RequestClientApp, QueryText, ResponseRowCount, RequestTarget

Stage 5: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
QueryTextmatch
  • _GetWatchlist("VIPUsers") transforms: term
  • _GetWatchlist('VIPUsers') transforms: term
  • vip1@email.com transforms: term
  • vip2@email.com transforms: term
field:"QueryText" kind:match

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
AADEmaileq[]excludes:AADEmail field:"AADEmail" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AADEmailproject
QueryTextproject
RequestClientAppproject
RequestTargetproject
ResponseRowCountproject
TimeGeneratedproject
AccountNameextend
AccountUPNSuffixextend
timestampextend

vCenter - Root impersonation

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

'Detects when root impersonation occurs.'

MITRE ATT&CK Coverage

TacticTechniques
Privilege Escalation

Rule Body

id: f1fcb22c-b459-42f2-a7ee-7276b5f1309c
name: vCenter - Root impersonation
description: |
  'Detects when root impersonation occurs.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CustomLogsAma
    dataTypes:
      - vcenter_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
 vCenter
  | where Message has_all ('ImpersonateUser', 'VcIntegrity', 'root')
  | extend user = 'root'
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: user
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

vCenter

Stage 2: where

| where Message has_all ('ImpersonateUser', 'VcIntegrity', 'root')

Stage 3: extend

| extend user = 'root'

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Messagematch
  • ImpersonateUser transforms: term
  • VcIntegrity transforms: term
  • root transforms: term
field:"Message" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
userextend

Back to all Kusto rules