Detection rules › Kusto

Kusto rules: 2g

Brute force attack against Azure Portal

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

Detects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations. Ref: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rule Body

id: 28b42356-45af-40a6-a0b4-a554cdfd5d8a
name: Brute force attack against Azure Portal
description: |
  Detects Azure Portal brute force attacks by monitoring for multiple authentication failures and a successful login within a 20-minute window. Default settings: 10 failures, 25 deviations.
  Ref: https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-sign-ins-error-codes.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // Set threshold value for deviation
  let threshold = 25;
  // Set the time range for the query
  let timeRange = 24h;
  // Set the authentication window duration
  let authenticationWindow = 20m;
  // Define a reusable function 'aadFunc' that takes a table name as input
  let aadFunc = (tableName: string) {
    // Query the specified table
    table(tableName)
    // Filter data within the last 24 hours
    | where TimeGenerated > ago(1d)
    // Filter records related to "Azure Portal" applications
    | where AppDisplayName has "Azure Portal"
    // Extract and transform some fields
    | extend
        DeviceDetail = todynamic(DeviceDetail),
        LocationDetails = todynamic(LocationDetails)
    | extend
        OS = tostring(DeviceDetail.operatingSystem),
        Browser = tostring(DeviceDetail.browser),
        State = tostring(LocationDetails.state),
        City = tostring(LocationDetails.city),
        Region = tostring(LocationDetails.countryOrRegion)
    // Categorize records as Success or Failure based on ResultType
    | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
    // Sort and identify sessions
    | sort by UserPrincipalName asc, TimeGenerated asc
    | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
    // Summarize data
    | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
    | summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
    // Filter records where "Success" occurs in the middle of a session
    | where array_index_of(list_FailureOrSuccess, "Success") != 0
    | where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
    // Remove unnecessary columns from the output
    | project-away SessionStartedUtc, list_FailureOrSuccess
    // Join with another table and calculate deviation
    | join kind=inner (
        table(tableName)
        | where TimeGenerated > ago(7d)
        | where AppDisplayName has "Azure Portal"
        | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
        | summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
    ) on UserPrincipalName
    | extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
    // Filter records based on deviation and failure count criteria
    | where Deviation > threshold and FailureCountBeforeSuccess >= 10
    // Expand the IPAddress array
    | mv-expand IPAddress
    | extend IPAddress = tostring(IPAddress)
    | extend timestamp = StartTime
  };
  // Call 'aadFunc' with different table names and union the results
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  // Additional transformation - Split UserPrincipalName
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
    - identifier: FullName
      columnName: UserPrincipalName
    - identifier: Name
      columnName: Name
    - identifier: UPNSuffix
      columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
    - identifier: AadUserId
      columnName: UserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 2.1.4
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 25;
let timeRange = 24h;
let authenticationWindow = 20m;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName: string) {
  table(tableName)
  | where TimeGenerated > ago(1d)
  | where AppDisplayName has "Azure Portal"
  | extend
      DeviceDetail = todynamic(DeviceDetail),
      LocationDetails = todynamic(LocationDetails)
  | extend
      OS = tostring(DeviceDetail.operatingSystem),
      Browser = tostring(DeviceDetail.browser),
      State = tostring(LocationDetails.state),
      City = tostring(LocationDetails.city),
      Region = tostring(LocationDetails.countryOrRegion)
  | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
  | sort by UserPrincipalName asc, TimeGenerated asc
  | extend SessionStartedUtc = row_window_session(TimeGenerated, timeRange, authenticationWindow, UserPrincipalName != prev(UserPrincipalName) or prev(FailureOrSuccess) == "Success")
  | summarize FailureOrSuccessCount = count() by  FailureOrSuccess, UserId, UserDisplayName, AppDisplayName, IPAddress, Browser, OS, State, City, Region, Type, CorrelationId, bin(TimeGenerated, authenticationWindow), ResultType, UserPrincipalName, SessionStartedUtc
  | summarize FailureCountBeforeSuccess = sumif(FailureOrSuccessCount, FailureOrSuccess == "Failure"), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), makelist(FailureOrSuccess), IPAddress = make_set(IPAddress, 15), make_set(Browser, 15), make_set(City, 15), make_set(State, 15), make_set(Region, 15), make_set(ResultType, 15) by SessionStartedUtc, UserPrincipalName, CorrelationId, AppDisplayName, UserId, Type
  | where array_index_of(list_FailureOrSuccess, "Success") != 0
  | where array_index_of(list_FailureOrSuccess, "Success") == array_length(list_FailureOrSuccess) - 1
  | project-away SessionStartedUtc, list_FailureOrSuccess
  | join kind=inner (
      table(tableName)
      | where TimeGenerated > ago(7d)
      | where AppDisplayName has "Azure Portal"
      | extend FailureOrSuccess = iff(ResultType in ("0", "50125", "50140", "70043", "70044"), "Success", "Failure")
      | summarize avgFailures = avg(todouble(FailureOrSuccess == "Failure")) by UserPrincipalName
  ) on UserPrincipalName
  | extend Deviation = abs(FailureCountBeforeSuccess - avgFailures) / avgFailures
  | where Deviation > threshold and FailureCountBeforeSuccess >= 10
  | mv-expand IPAddress
  | extend IPAddress = tostring(IPAddress)
  | extend timestamp = StartTime
};

Union isfuzzy=true (2 Sources)

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

Leg 1: aadSignin

Leg 2: aadNonInt

Applied to the Combined Result

| extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])

Output Fields

These fields are emitted when the rule matches.

FieldSource
Nameextend
UPNSuffixextend

CTE - Rule Capacity Critical (95%)

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

Tenant-wide blocking-rule/IOC capacity has reached 95% - new rules or IOCs may soon fail to be added. Emitted by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js) when usage newly crosses this threshold, and re-emitted at most once per ~30 days while it remains above threshold.

Rule Body

id: bd06aa7c-8aef-40f3-bf32-a70b5d48f79e
name: CTE - Rule Capacity Critical (95%)
description: |
  Tenant-wide blocking-rule/IOC capacity has reached 95% - new rules or IOCs may soon fail to be added. Emitted by the daily tenant health-check job (api/src/functions/tenantHealthChecks.js) when usage newly crosses this threshold, and re-emitted at most once per ~30 days while it remains above threshold.
severity: High
requiredDataConnectors:
  - connectorId: cybereinforce_cte
    dataTypes:
      - CybereinforceCTE_CL
queryFrequency: 24h
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
  []
relevantTechniques: []
query: |
  CybereinforceCTE_CL
  | where EventType == "rule.capacity_critical"
  | extend d = parse_json(Details)
  | extend AppName = "Cybereinforce", RuleCount = toint(d.ruleCount), RuleLimit = toint(d.ruleLimit), UsedPct = todouble(d.usedPct)
  | project TimeGenerated, TenantId, AppName, RuleCount, RuleLimit, UsedPct, Severity, Category, Source
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: AppName
      - identifier: InstanceName
        columnName: TenantId
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

CybereinforceCTE_CL

Stage 2: where

| where EventType == "rule.capacity_critical"

Stage 3: extend

| extend d = parse_json(Details)

Stage 4: extend

| extend AppName = "Cybereinforce", RuleCount = toint(d.ruleCount), RuleLimit = toint(d.ruleLimit), UsedPct = todouble(d.usedPct)

Stage 5: project

| project TimeGenerated, TenantId, AppName, RuleCount, RuleLimit, UsedPct, Severity, Category, Source

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • rule.capacity_critical
field:"EventType" kind:eq value:"rule.capacity_critical"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppNameproject
Categoryproject
RuleCountproject
RuleLimitproject
Severityproject
Sourceproject
TenantIdproject
TimeGeneratedproject
UsedPctproject

CYFIRMA - Attack Surface - Cloud Weakness 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 cloud storage buckets (e.g., AWS S3) that are publicly accessible without authentication. Such misconfigurations can lead to data exfiltration, compliance violations, and reputational damage. The detection is based on Cyfirma's Attack Surface Intelligence."

MITRE ATT&CK Coverage

Rule Body

id: b8a3c5e2-04d5-4b61-9b62-b4f53a417f74 
name: CYFIRMA - Attack Surface - Cloud Weakness Medium Rule
description: |
  "This rule detects cloud storage buckets (e.g., AWS S3) that are publicly accessible without authentication. 
  Such misconfigurations can lead to data exfiltration, compliance violations, and reputational damage. 
  The detection is based on Cyfirma's Attack Surface Intelligence."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: CyfirmaAttackSurfaceAlertsConnector
    dataTypes:
      - CyfirmaASCloudWeaknessAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Collection
  - Discovery
  - Exfiltration
relevantTechniques:
  - T1087
  - T1087.004
query: |
  // Medium Severity - Attack Surface - Cloud Weakness - Unauthorized Public Cloud Storage Exposure Detected
  let timeFrame = 5m;
  CyfirmaASCloudWeaknessAlerts_CL
  | where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      Domain=asset_name,
      AlertUID=alert_uid,
      UID=uid,
      Source=source,
      SourceType=source_type,
      CreatedDate=created_date,
      Impact=impact,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      Domain,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      Source,
      SourceType,
      CreatedDate,
      Impact,
      ProviderName,
      ProductName
entityMappings:
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
customDetails:
  TimeGenerated: TimeGenerated
  RiskScore: RiskScore
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
  Source: Source
  SourceType: SourceType
  CreatedDate: CreatedDate
  Impact: Impact
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Medium Severity Alert - Unauthorized Public Cloud Storage Exposure Detected - Domain: {{Domain}}"
  alertDescriptionFormat: "CYFIRMA - Medium Severity Alert - Unauthorized Public Cloud Storage Exposure Detected - {{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

CyfirmaASCloudWeaknessAlerts_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=asset_name,
    AlertUID=alert_uid,
    UID=uid,
    Source=source,
    SourceType=source_type,
    CreatedDate=created_date,
    Impact=impact,
    ProviderName='CYFIRMA',
    ProductName='DeCYFIR/DeTCT'

Stage 4: project

| project
    TimeGenerated,
    Description,
    Domain,
    RiskScore,
    FirstSeen,
    LastSeen,
    AlertUID,
    UID,
    Source,
    SourceType,
    CreatedDate,
    Impact,
    ProviderName,
    ProductName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertUIDproject
CreatedDateproject
Descriptionproject
Domainproject
FirstSeenproject
Impactproject
LastSeenproject
ProductNameproject
ProviderNameproject
RiskScoreproject
Sourceproject
SourceTypeproject
TimeGeneratedproject
UIDproject

CYFIRMA - High severity Command & Control Network Indicators with Block Recommendation 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
high
Time window
5m
Source
github.com/Azure/Azure-Sentinel

"This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure. These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."

MITRE ATT&CK Coverage

Rule Body

id: 6b61b716-afd9-4f6c-ad00-965d5987cafd
name: CYFIRMA - High severity Command & Control Network Indicators with Block Recommendation Rule
description: |
  "This analytics rule detects network-based indicators identified by CYFIRMA threat intelligence as associated with Command & Control (C2) infrastructure.
  These may represent attacker-controlled endpoints used to maintain persistence, exfiltrate data, or receive commands from malware-infected hosts."
version: 1.0.1
kind: Scheduled
severity: High
enabled: false
requiredDataConnectors:
  - connectorId: CyfirmaCyberIntelligenceDC
    dataTypes:
      - CyfirmaIndicators_CL
query: |
  // Network Indicators query with Block Recommended Action and C2 Roles
  let timeFrame= 5m;
  CyfirmaIndicators_CL 
  | where ConfidenceScore >= 80
      and TimeGenerated between (ago(timeFrame) .. now())
      and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains 'c2' or Roles contains 'Command & Control')
  | 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: gt
triggerThreshold: 0
suppressionDuration: 5m
suppressionEnabled: true
tactics:
  - CommandAndControl
  - InitialAccess
  - Exfiltration
relevantTechniques:
  - T1071
  - T1090
  - T1573
  - T1105
  - T1568
  - T1566
  - T1041
  - T1071.001
  - T1573.001
  - T1568.002
  - T1566.002
alertDetailsOverride:
  alertDisplayNameFormat: "High-Confidence Command & Control Network Indicators with Block Recommendation Rule - {{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 TimeGenerated between (ago(timeFrame) .. now())
    and pattern !contains 'file:hashes' and RecommendedActions has 'Block' and (Roles contains 'c2' or Roles contains 'Command & Control')

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

Detect threat information in web requests (ASIM Web Session)

#
Status
available
Severity
high
Time window
5m
Group by
DstIpAddr, SrcHostname, SrcIpAddr, SrcUsername, ThreatCategory, ThreatField, ThreatName, ThreatOriginalConfidence, ThreatRiskLevel, Url
Source
github.com/Azure/Azure-Sentinel

'This rule would generate an alert if EvenSeverity is 'High' or 'ThreatRiskLevel' or 'ThreatOriginalConfidence' value is greater than 90.

MITRE ATT&CK Coverage

Rule Body

id: 7d2ed1c7-da26-45fd-b4ea-b6f2bbeccea7
name: Detect threat information in web requests (ASIM Web Session)
description: |
  'This rule would generate an alert if EvenSeverity is 'High' or 'ThreatRiskLevel' or 'ThreatOriginalConfidence' value is greater than 90.
severity: High
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
  - T1133
query: |
  let lookback= 5m;
  _Im_WebSession(starttime=ago(lookback))
  | project
      EventSeverity,
      ThreatName,
      ThreatCategory,
      ThreatRiskLevel,
      ThreatOriginalConfidence,
      ThreatField,
      TimeGenerated,
      SrcIpAddr,
      SrcUsername,
      SrcHostname,
      Url,
      DstIpAddr
  | where ThreatRiskLevel >= 90
      or toint(ThreatOriginalConfidence) >= 90
      or EventSeverity =~ "High"
  | summarize
      EventCount = count(),
      EventStartTime=min(TimeGenerated),
      EvenEndTime=max(TimeGenerated)
      by
      SrcIpAddr,
      SrcUsername,
      SrcHostname,
      Url,
      DstIpAddr,
      ThreatName,
      ThreatCategory,
      ThreatRiskLevel,
      ThreatOriginalConfidence,
      ThreatField
  | extend
      Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
      UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: DstIpAddr
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  ThreatCategory: ThreatCategory
  ThreatConfidence: ThreatOriginalConfidence
  ThreatName: ThreatName
  EventStartTime: EventStartTime
  EvenEndTime: EvenEndTime
  EventCount: EventCount
alertDetailsOverride:
  alertDisplayNameFormat: "User '{{SrcUsername}}' with IP address '{{SrcIpAddr}}' has been identified as being associated with a threat named '{{ThreatName}}'"
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lookback = 5m;

Stage 1: source

_Im_WebSession(starttime=ago(lookback))

Stage 2: project

| project
    EventSeverity,
    ThreatName,
    ThreatCategory,
    ThreatRiskLevel,
    ThreatOriginalConfidence,
    ThreatField,
    TimeGenerated,
    SrcIpAddr,
    SrcUsername,
    SrcHostname,
    Url,
    DstIpAddr

Stage 3: where

| where ThreatRiskLevel >= 90
    or toint(ThreatOriginalConfidence) >= 90
    or EventSeverity =~ "High"

Stage 4: summarize

| summarize
    EventCount = count(),
    EventStartTime=min(TimeGenerated),
    EvenEndTime=max(TimeGenerated)
    by
    SrcIpAddr,
    SrcUsername,
    SrcHostname,
    Url,
    DstIpAddr,
    ThreatName,
    ThreatCategory,
    ThreatRiskLevel,
    ThreatOriginalConfidence,
    ThreatField

Stage 5: extend

| extend
    Name = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 0)[0]), SrcUsername),
    UPNSuffix = iif(SrcUsername contains "@", tostring(split(SrcUsername, '@', 1)[0]), "")

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DstIpAddrsummarize
EvenEndTimesummarize
EventCountsummarize
EventStartTimesummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
ThreatCategorysummarize
ThreatFieldsummarize
ThreatNamesummarize
ThreatOriginalConfidencesummarize
ThreatRiskLevelsummarize
Urlsummarize
Nameextend
UPNSuffixextend

Excessive number of HTTP authentication failures from a source (ASIM Web Session schema)

#
Severity
low
Time window
1d
Group by
SrcIpAddr, UrlHost
Author
Yaron
Source
github.com/Azure/Azure-Sentinel

This rule identifies a source that repeatedly fails to authenticate to a web service (HTTP response code 403). This may indicate a brute force or credential stuffing attack. This rule uses the Advanced Security Information Model (ASIM) and supports any web session source that complies with ASIM.

MITRE ATT&CK Coverage

Rule Body

id: a1bddaf8-982b-4089-ba9e-6590dfcf80ea
name: Excessive number of HTTP authentication failures from a source (ASIM Web Session schema)
description: |
    This rule identifies a source that repeatedly fails to authenticate to a web service (HTTP response code 403). This may indicate a [brute force](https://en.wikipedia.org/wiki/Brute-force_attack) or [credential stuffing](https://en.wikipedia.org/wiki/Credential_stuffing) attack.
    This rule uses the [Advanced Security Information Model (ASIM)](https://aka.ms/AboutASIM) and supports any web session source that complies with ASIM.
severity: Low
requiredDataConnectors:
  - connectorId: SquidProxy
    dataTypes:
      - SquidProxy_CL
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
    - Persistence
    - CredentialAccess
relevantTechniques:
    - T1110
    - T1556
tags:
    - ParentAlert: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/CommonSecurityLog/Wazuh-Large%20Number%20of%20Web%20errors%20from%20an%20IP.yaml
      ParentVersion: 1.1.0
    - Schema: ASIMWebSession
      SchemaVersion: 0.2.1
query: |
    let error403_count_threshold=200;
    _Im_WebSession(eventresultdetails_in=dynamic(["403"]))
    | extend ParsedUrl=parse_url(Url)
    | extend UrlHost=tostring(ParsedUrl["Host"]), UrlSchema=tostring(ParsedUrl["Schema"])
    | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), NumberOfErrors = count(), Urls=makeset(Url) by UrlHost, SrcIpAddr
    | where NumberOfErrors > error403_count_threshold
    | sort by NumberOfErrors desc
    | extend Url=tostring(Urls[0])
entityMappings:
    - entityType: URL
      fieldMappings:
          - identifier: Url
            columnName: Url
    - entityType: IP
      fieldMappings:
          - identifier: Address
            columnName: SrcIpAddr
customDetails:
  NumberOfErrors: NumberOfErrors
alertDetailsOverride:
    alertDisplayNameFormat: Excessive number of HTTP authentication failures from {{SrcIpAddr}
    alertDescriptionFormat: A client with address {{SrcIpAddr}} generated a large number of failed authentication HTTP requests. This may indicate a [brute force](https://en.wikipedia.org/wiki/Brute-force_attack) or [credential stuffing](https://en.wikipedia.org/wiki/Credential_stuffing) attack.
version: 1.0.6
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Yaron
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let error403_count_threshold = 200;

Stage 1: source

_Im_WebSession(eventresultdetails_in=dynamic(["403"]))

Stage 2: extend

| extend ParsedUrl=parse_url(Url)

Stage 3: extend

| extend UrlHost=tostring(ParsedUrl["Host"]), UrlSchema=tostring(ParsedUrl["Schema"])

Stage 4: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), NumberOfErrors = count(), Urls=makeset(Url) by UrlHost, SrcIpAddr

Stage 5: where

| where NumberOfErrors > error403_count_threshold

Stage 6: sort

| sort by NumberOfErrors desc

Stage 7: extend

| extend Url=tostring(Urls[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
NumberOfErrorsgt
  • 200 transforms: number
field:"NumberOfErrors" kind:gt value:"200"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
NumberOfErrorssummarize
SrcIpAddrsummarize
StartTimesummarize
UrlHostsummarize
Urlssummarize
Urlextend

Malicious BEC Inbox Rule

#
Severity
medium
Time window
1d
Group by
ClientIPAddress, Keyword, OfficeObjectId, Operation, OriginatingServer, ResultStatus, RuleDetail, UserId
Source
github.com/Azure/Azure-Sentinel

'Often times after the initial compromise in a BEC attack the attackers create inbox rules to delete emails that contain certain keywords related to their BEC attack. This is done so as to limit ability to warn compromised users that they've been compromised.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 8ac77493-3cae-4840-8634-15fb23f8fb68
name: Malicious BEC Inbox Rule
description: |
  'Often times after the initial compromise in a BEC attack the attackers create inbox rules to delete emails that contain certain keywords related to their BEC attack.
   This is done so as to limit ability to warn compromised users that they've been compromised. 
severity: Medium
requiredDataConnectors:
  - connectorId: Office365
    dataTypes:
      - OfficeActivity
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - DefenseEvasion
relevantTechniques:
  - T1098
  - T1078
tags:
  - BEC
query: |
 let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);
 OfficeActivity
 | where Operation =~ "New-InboxRule"
 | where Parameters has "Deleted Items" or Parameters has "Junk Email"  or Parameters has "DeleteMessage"
 | extend Events=todynamic(Parameters)
 | parse Events  with * "SubjectContainsWords" SubjectContainsWords '}'*
 | parse Events  with * "BodyContainsWords" BodyContainsWords '}'*
 | parse Events  with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*
 | where SubjectContainsWords has_any (BEC_Keywords)
  or BodyContainsWords has_any (BEC_Keywords)
  or SubjectOrBodyContainsWords has_any (BEC_Keywords)
 | extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
 | extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
 | extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
 | summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by  Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail
 | extend UserName = split(UserId, '@')[0], DomainName = split(UserId, '@')[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserId
      - identifier: Name
        columnName: UserName
      - identifier: UPNSuffix
        columnName: DomainName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIPAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Let Binding: BEC_Keywords used in Stage 8

let BEC_Keywords = dynamic([ 'invoice','payment','paycheck','transfer','bank statement','bank details','closing','funds','bank account','account details','remittance','purchase','deposit',"PO#","Zahlung","Rechnung","Paiement", "virement bancaire","Bankuberweisung",'hacked','phishing']);

Stage 1: source

OfficeActivity

Stage 2: where

| where Operation =~ "New-InboxRule"

Stage 3: where

| where Parameters has "Deleted Items" or Parameters has "Junk Email"  or Parameters has "DeleteMessage"

Stage 4: extend

| extend Events=todynamic(Parameters)

Stage 5: parse

| parse Events  with * "SubjectContainsWords" SubjectContainsWords '}'*

Stage 6: parse

| parse Events  with * "BodyContainsWords" BodyContainsWords '}'*

Stage 7: parse

| parse Events  with * "SubjectOrBodyContainsWords" SubjectOrBodyContainsWords '}'*

Stage 8: where

| where SubjectContainsWords has_any (BEC_Keywords)
 or BodyContainsWords has_any (BEC_Keywords)
 or SubjectOrBodyContainsWords has_any (BEC_Keywords)

Stage 9: extend (3 consecutive steps)

| extend ClientIPAddress = case( ClientIP has ".", tostring(split(ClientIP,":")[0]), ClientIP has "[", tostring(trim_start(@'[[]',tostring(split(ClientIP,"]")[0]))), ClientIP )
| extend Keyword = iff(isnotempty(SubjectContainsWords), SubjectContainsWords, (iff(isnotempty(BodyContainsWords),BodyContainsWords,SubjectOrBodyContainsWords )))
| extend RuleDetail = case(OfficeObjectId contains '/' , tostring(split(OfficeObjectId, '/')[-1]) , tostring(split(OfficeObjectId, '\\')[-1]))
ClientIPAddress =
ifClientIP has "."tostring(split(ClientIP, ":")[0])
elifClientIP has "["tostring(trim_start(@'[[]', tostring(split(ClientIP, "]")[0])))
elseClientIP

Stage 10: summarize

| summarize count(), StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by  Operation, UserId, ClientIPAddress, ResultStatus, Keyword, OriginatingServer, OfficeObjectId, RuleDetail

Stage 11: extend

| extend UserName = split(UserId, '@')[0], DomainName = split(UserId, '@')[1]

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
BodyContainsWordsmatch
  • Bankuberweisung transforms: term
  • PO# transforms: term
  • Paiement transforms: term
  • Rechnung transforms: term
  • Zahlung transforms: term
  • account details transforms: term
  • bank account transforms: term
  • bank details transforms: term
  • bank statement transforms: term
  • closing transforms: term
  • deposit transforms: term
  • funds transforms: term
  • hacked transforms: term
  • invoice transforms: term
  • paycheck transforms: term
  • payment transforms: term
  • phishing transforms: term corpus 3 (kusto 3)
  • purchase transforms: term
  • remittance transforms: term
  • transfer transforms: term
  • virement bancaire transforms: term
field:"BodyContainsWords" kind:match
Operationeq
  • New-InboxRule corpus 5 (splunk 2, kusto 2, sigma 1)
field:"Operation" kind:eq value:"New-InboxRule"
Parametersmatch
  • DeleteMessage transforms: term corpus 3 (kusto 3)
  • Deleted Items transforms: term corpus 3 (kusto 3)
  • Junk Email transforms: term corpus 3 (kusto 3)
field:"m365::Parameters" kind:match
SubjectContainsWordsmatch
  • Bankuberweisung transforms: term
  • PO# transforms: term
  • Paiement transforms: term
  • Rechnung transforms: term
  • Zahlung transforms: term
  • account details transforms: term
  • bank account transforms: term
  • bank details transforms: term
  • bank statement transforms: term
  • closing transforms: term
  • deposit transforms: term
  • funds transforms: term
  • hacked transforms: term
  • invoice transforms: term
  • paycheck transforms: term
  • payment transforms: term
  • phishing transforms: term corpus 3 (kusto 3)
  • purchase transforms: term
  • remittance transforms: term
  • transfer transforms: term
  • virement bancaire transforms: term
field:"SubjectContainsWords" kind:match
SubjectOrBodyContainsWordsmatch
  • Bankuberweisung transforms: term
  • PO# transforms: term
  • Paiement transforms: term
  • Rechnung transforms: term
  • Zahlung transforms: term
  • account details transforms: term
  • bank account transforms: term
  • bank details transforms: term
  • bank statement transforms: term
  • closing transforms: term
  • deposit transforms: term
  • funds transforms: term
  • hacked transforms: term
  • invoice transforms: term
  • paycheck transforms: term
  • payment transforms: term
  • phishing transforms: term corpus 3 (kusto 3)
  • purchase transforms: term
  • remittance transforms: term
  • transfer transforms: term
  • virement bancaire transforms: term
field:"SubjectOrBodyContainsWords" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPAddresssummarize
EndTimeUtcsummarize
Keywordsummarize
OfficeObjectIdsummarize
Operationsummarize
OriginatingServersummarize
ResultStatussummarize
RuleDetailsummarize
StartTimeUtcsummarize
UserIdsummarize
DomainNameextend
UserNameextend

NRT Authentication Methods Changed for VIP Users

#
Severity
medium
Group by
InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Result, Target
Source
github.com/Azure/Azure-Sentinel

Identifies authentication methods being changed for a list of VIP users watchlist. This could be an indication of an attacker adding an auth method to the account so they can have continued access.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 29e99017-e28d-47be-8b9a-c8c711f8a903
name: NRT Authentication Methods Changed for VIP Users 
description: |
  'Identifies authentication methods being changed for a list of VIP users watchlist. This could be an indication of an attacker adding an auth method to the account so they can have continued access.'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
relevantTechniques:
  - T1098
tags:
  - AADSecOpsGuide
query: |
  let security_info_actions = dynamic(["User registered security info", "User changed default security info", "User deleted security info", "Admin updated security info", "User reviewed security info", "Admin deleted security info", "Admin registered security info"]);
  let VIPUsers = _GetWatchlist('VIPUsers')
    | extend ["User Principal Name"] = column_ifexists("User Principal Name","")
    | distinct ["User Principal Name"];
  AuditLogs
  | where Category =~ "UserManagement"
  | where ActivityDisplayName in (security_info_actions)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | mv-apply TargetResource = TargetResources on 
    (
        where TargetResource.type =~ "User"
        | extend Target = trim(@'"',tolower(tostring(TargetResource.userPrincipalName)))
    )
  | where Target in~ (VIPUsers)
  | summarize Start=min(TimeGenerated), End=max(TimeGenerated), Actions = make_set(ResultReason) by InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result, Target
  | extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0])
  | extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatedByName
      - identifier: UPNSuffix
        columnName: InitiatedByUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.0.6
kind: NRT

Stages and Predicates

Let Binding: security_info_actions used in Stage 3

let security_info_actions = dynamic(["User registered security info", "User changed default security info", "User deleted security info", "Admin updated security info", "User reviewed security info", "Admin deleted security info", "Admin registered security info"]);

Let Binding: VIPUsers used in Stage 6

let VIPUsers = _GetWatchlist('VIPUsers')
  | extend ["User Principal Name"] = column_ifexists("User Principal Name","")
  | distinct ["User Principal Name"];

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "UserManagement"

Stage 3: where

| where ActivityDisplayName in (security_info_actions)

Stage 4: extend (5 consecutive steps)

| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))

Stage 5: kusto:mv-apply

| mv-apply TargetResource = TargetResources on 
  (
      where TargetResource.type =~ "User"
      | extend Target = trim(@'"',tolower(tostring(TargetResource.userPrincipalName)))
  )

Stage 6: where

| where Target in~ (VIPUsers)

Stage 7: summarize

| summarize Start=min(TimeGenerated), End=max(TimeGenerated), Actions = make_set(ResultReason) by InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result, Target

Stage 8: extend

| extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0])

Stage 9: extend

| extend InitiatedByName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatedByUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActivityDisplayNamein
  • Admin deleted security info
  • Admin registered security info
  • Admin updated security info
  • User changed default security info
  • User deleted security info
  • User registered security info
  • User reviewed security info
field:"azure_ad::activity_display_name" kind:in
Categoryeq
  • UserManagement corpus 7 (sigma 4, kusto 3)
field:"Category" kind:eq value:"UserManagement"
Targetin
  • VIPUsers
field:"Target" kind:in value:"VIPUsers"
TargetResource.typeeq
  • User transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"User"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionssummarize
Endsummarize
InitiatingAadUserIdsummarize
InitiatingAppNamesummarize
InitiatingAppServicePrincipalIdsummarize
InitiatingIpAddresssummarize
InitiatingUserPrincipalNamesummarize
Resultsummarize
Startsummarize
Targetsummarize
TargetNameextend
TargetUPNSuffixextend
InitiatedByNameextend
InitiatedByUPNSuffixextend

Okta Fast Pass phishing Detection

#
Status
available
Severity
medium
Time window
1h
Group by
actor_alternateId_s, actor_displayName_s, client_geographicalContext_city_s, client_geographicalContext_country_s, client_geographicalContext_state_s, client_ipAddress_s, client_userAgent_browser_s, client_userAgent_os_s, client_userAgent_rawUserAgent_s, displayMessage_s, outcome_reason_s, outcome_result_s, severity_s
Source
github.com/Azure/Azure-Sentinel

This query detects cases in which Okta FastPass effectively prevented access to a known phishing website.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 78d2b06c-8dc0-40e1-91c8-66d916c186f3
name: Okta Fast Pass phishing Detection
description: |
  This query detects cases in which Okta FastPass effectively prevented access to a known phishing website.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: OktaSSO
    dataTypes:
      - Okta_CL
  - connectorId: OktaSSOv2
    dataTypes:
      - OktaSSO
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566.002
tags:
  - Okta
query: |
 // Add any known good IPs to this list to reduce noise
 let AllowedIPList = dynamic([]);
 OktaSSO
 | where isnotempty(client_ipAddress_s) and client_ipAddress_s !in (AllowedIPList)
 | where eventType_s == 'user.authentication.auth_via_mfa'
 | where outcome_result_s == 'FAILURE'
 | where outcome_reason_s == 'FastPass declined phishing attempt'
 | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_ipAddress_s, client_geographicalContext_state_s,displayMessage_s, outcome_result_s,
  outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_userAgent_rawUserAgent_s,client_userAgent_browser_s, severity_s, client_geographicalContext_city_s, client_geographicalContext_country_s
 | extend Location = strcat(client_geographicalContext_city_s, "-", client_geographicalContext_country_s), AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: client_ipAddress_s
customDetails:
  UserAgent: client_userAgent_rawUserAgent_s
  UserAgentOS: client_userAgent_os_s
  UserAgentBrowser: client_userAgent_browser_s
  Location: Location
alertDetailsOverride:
  alertDisplayNameFormat: 'Okta FastPass phishing attempt declined for {{actor_alternateId_s}}'
  alertDescriptionFormat: 'Okta FastPass declined a phishing attempt for {{actor_alternateId_s}} from {{client_ipAddress_s}} using {{client_userAgent_browser_s}}.'
version: 1.1.2
kind: Scheduled

Stages and Predicates

Parameters

let AllowedIPList = dynamic([]);

Stage 1: source

OktaSSO

Stage 2: where

| where isnotempty(client_ipAddress_s) and client_ipAddress_s !in (AllowedIPList)

Stage 3: where

| where eventType_s == 'user.authentication.auth_via_mfa'

Stage 4: where

| where outcome_result_s == 'FAILURE'

Stage 5: where

| where outcome_reason_s == 'FastPass declined phishing attempt'

Stage 6: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by actor_alternateId_s, actor_displayName_s, client_userAgent_os_s, client_ipAddress_s, client_geographicalContext_state_s,displayMessage_s, outcome_result_s,
 outcome_reason_s, column_ifexists('debugContext_debugData_logOnlySecurityData_s', ""), column_ifexists('debugContext_debugData_threatSuspected_s', ""), client_userAgent_rawUserAgent_s,client_userAgent_browser_s, severity_s, client_geographicalContext_city_s, client_geographicalContext_country_s

Stage 7: extend

| extend Location = strcat(client_geographicalContext_city_s, "-", client_geographicalContext_country_s), AccountName = tostring(split(actor_alternateId_s, "@")[0]), AccountUPNSuffix = tostring(split(actor_alternateId_s, "@")[1])

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
client_ipAddress_seq[]excludes:client_ipAddress_s field:"client_ipAddress_s" value:"[]"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EndTimesummarize
StartTimesummarize
actor_alternateId_ssummarize
actor_displayName_ssummarize
client_geographicalContext_city_ssummarize
client_geographicalContext_country_ssummarize
client_geographicalContext_state_ssummarize
client_ipAddress_ssummarize
client_userAgent_browser_ssummarize
client_userAgent_os_ssummarize
client_userAgent_rawUserAgent_ssummarize
displayMessage_ssummarize
outcome_reason_ssummarize
outcome_result_ssummarize
severity_ssummarize
AccountNameextend
AccountUPNSuffixextend
Locationextend

Spearphishing Attachment: ISO Images (Microsoft Sentinel)

#
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. WARNING: Check your Sysmon parsing functions and verify you have the logs. Using "Rendered Description" field for parsing causes parsing issues for registry events. 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
//
//
let parse_sysmon_events = (T:(TimeGenerated:datetime,EventID:int, Source:string, EventData:string))
{
T 
| where Source == "Microsoft-Windows-Sysmon"
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
};
// Query Parameters
let lookback = 2h;
Event
| where TimeGenerated > ago(lookback)
| where EventID == 11
| invoke parse_sysmon_events()
| where tostring(TargetFilename) endswith ".iso.lnk" or tostring(TargetFilename) 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
//
//
let parse_sysmon_events = (T:(TimeGenerated:datetime,EventID:int, Source:string, EventData:string))
{
T 
| where Source == "Microsoft-Windows-Sysmon"
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
};
// Query Parameters
let lookback = 2h;
Event
| where TimeGenerated > ago(lookback)
| where EventID == 13
| invoke parse_sysmon_events()
| where TargetObject startswith @"HKLM\SYSTEM\MountedDevices\\DosDevices"
| extend Folder = replace(@'.*\\([A-Z]:)',@'\1\\',tostring(TargetObject))
| join kind = inner 
    (
    Event
    | where TimeGenerated > ago(lookback)
    | where EventID == 1
    | invoke parse_sysmon_events()
    | extend Folder = tostring(CurrentDirectory)
    ) on Computer, 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
//
//
let parse_sysmon_events = (T:(TimeGenerated:datetime,EventID:int, Source:string, EventData:string))
{
T 
| where Source == "Microsoft-Windows-Sysmon"
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
};
// Query Parameters
let lookback = 30h;
Event
| where TimeGenerated > ago(lookback)
| where EventID == 13
| invoke parse_sysmon_events()
| where TargetObject startswith @"HKLM\SYSTEM\MountedDevices\\DosDevices"
| extend Folder = replace(@'.*\\([A-Z]:)',@'\1\\',tostring(TargetObject))
| join kind = inner 
    (
    Event
    | where TimeGenerated > ago(lookback)
    | where EventID == 3
    | invoke parse_sysmon_events()
    | extend Folder = replace(@'^([A-Z]:\\).*',@'\1',tostring(Image))
    ) on Computer, Folder
// If needed, exclude the legitimate activity and servers

Stages and Predicates

Parameters

let lookback = 2h;

Let Binding: parse_sysmon_events

let parse_sysmon_events = (T:(TimeGenerated:datetime,EventID:int, Source:string, EventData:string))
{
T 
| where Source == "Microsoft-Windows-Sysmon"
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
};

Stage 1: source

Event

Stage 2: where

where TimeGenerated > ago(7200s)

Stage 3: where

where EventID == 11

Stage 4: where

where Source == "Microsoft-Windows-Sysmon"

Stage 5: extend

extend EventData

Stage 6: mv-expand

mv-expand EventData

Stage 7: evaluate

evaluate

Stage 8: extend

extend Key, Value

Stage 9: evaluate

evaluate

Stage 10: where

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

Stage 11: source

Event

Stage 12: where

where TimeGenerated > ago(7200s)

Stage 13: where

where EventID == 13

Stage 14: where

where Source == "Microsoft-Windows-Sysmon"

Stage 15: extend

extend EventData

Stage 16: mv-expand

mv-expand EventData

Stage 17: evaluate

evaluate

Stage 18: extend

extend Key, Value

Stage 19: evaluate

evaluate

Stage 20: where

where TargetObject startswith @"HKLM\SYSTEM\MountedDevices\\DosDevices"

Stage 21: extend

extend Folder

Stage 22: join

join kind=inner (Event) on Computer, Folder

Stage 23: source

Event

Stage 24: where

where TimeGenerated > ago(108000s)

Stage 25: where

where EventID == 13

Stage 26: where

where Source == "Microsoft-Windows-Sysmon"

Stage 27: extend

extend EventData

Stage 28: mv-expand

mv-expand EventData

Stage 29: evaluate

evaluate

Stage 30: extend

extend Key, Value

Stage 31: evaluate

evaluate

Stage 32: where

where TargetObject startswith @"HKLM\SYSTEM\MountedDevices\\DosDevices"

Stage 33: extend

extend Folder

Stage 34: join

join kind=inner (Event) on Computer, Folder

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 1
  • 11 corpus 2 (kusto 2)
  • 13
  • 3
field:"EventID" kind:eq
Sourceeq
  • Microsoft-Windows-Sysmon
field:"Source" kind:eq value:"Microsoft-Windows-Sysmon"
TargetFilenameends_with
  • .img.lnk transforms: tostring
  • .iso.lnk transforms: tostring
field:"TargetFilename" kind:ends_with
TargetObjectstarts_with
  • HKLM\SYSTEM\MountedDevices\\DosDevices
field:"TargetObject" kind:starts_with value:"HKLM\SYSTEM\MountedDevices\\DosDevices"

Output Fields

These fields are emitted when the rule matches.

FieldSource
EventDataextend
Keyextend
Valueextend

Suspicious named pipes

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

This query looks for Named Pipe events that either contain one of the known IOCs or make use of patterns that can be linked to CobaltStrike usage.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: ddf7c669-db26-4215-acaf-11e2953a04e6
name: Suspicious named pipes
description: |
  This query looks for Named Pipe events that either contain one of the known IOCs or make use of patterns that can be linked to CobaltStrike usage.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
  - DefenseEvasion
relevantTechniques:
  - T1559
  - T1055
query: |
  let timeframe=1h;
  let CobaltStrikeDefaults= dynamic([@"msagent_", @"MSSE-", @"postex_", @"status_", @"mypipe-f", @"mypipe-h",@"ntsvcs_",@"scerpc_", @"mojo.5688.8052."]);
  let CobaltStrikeMallable= dynamic([@"win_svc", @"ntsvcs", @"scerpc", @"status_", @"SearchTextHarvester", @"DserNamePipe",@"wkssvc_",@"scerpc_", @"spoolss_",@"CatalogChangeListener",@"fullduplex_",@"demoagent_",@"PGMessagePipe",@"MsFteWds",@"postex_ssh_",@"windows.update.manager",@"\f4c3",@"\f53f",@"halfduplex_"]);
  DeviceEvents
  | where Timestamp >= ago(timeframe)
  | where ActionType == "NamedPipeEvent"
  | extend AdditionalFields=parse_json(AdditionalFields)
  | extend ThreadId=tostring(AdditionalFields.ThreadId)
  | extend PipeName=tostring(AdditionalFields.PipeName)
  // creating string based variants of the processIDs for matching several times later
  | extend InitiatingPID=tostring(InitiatingProcessId)
  | extend InitiatingParentPID=tostring(InitiatingProcessParentId)
  // Begin allow-list.
  // End allow-list.
  | where PipeName has_any (CobaltStrikeDefaults) or
  // Mojo is generated by Chrome(ium) browsers and teams and have distinct pattern including the (parent)ProcessId and ThreadId plus a random character string, CobaltStrike generates hex.
        (PipeName matches regex @"\\mojo\.\d+\.\d+\." and not(PipeName matches regex @"\\mojo\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
  // Chrome(ium) browsers sync processes have distinct pattern including the (parent)ProcessId and ThreadId plus a random character string, CobaltStrike generates hex.
        (PipeName matches regex @"\\(edge|chrome)\.sync\.\d+\.\d+\." and not(PipeName matches regex @"\\(edge|chrome|edge\.sync|chrome\.sync)\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
  // PSHost is generated by PowerShell and has a distinct pattern including the (parent)ProcessId.
        (PipeName matches regex @"\\PSHost\.\d+\." and not(PipeName matches regex @"\\PSHost\.\d+\.\d+\." or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
  // Crashpad pipes have a distinct pattern including the ProcessId and a string of upper case characters.
        (PipeName matches regex @"\\crashpad_" and not(PipeName matches regex @"\\crashpad_\d+_[A-Z]+" or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
  // Firefox pipes have a distinct pattern including the ProcessId and 1-3 digits which are sequential for each new pipe.
        (PipeName matches regex @"\\cubeb-pipe-" and not(PipeName matches regex @"\\cubeb-pipe-\d+_[0-9]{1-3}+" or PipeName has InitiatingPID)) or
  // Based on a list of public mallable profiles and a suffix that is a random HEX string.
        (PipeName has_any (CobaltStrikeMallable) and PipeName matches regex @"[a-fA-F0-9]{2,10}$") or
        (PipeName matches regex @"\\pipe\\[0-9a-f]{7,10}" or PipeName matches regex @"\\pipe\\[0-9a-f]{8}")

entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: AccountSid
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1h;
let CobaltStrikeDefaults = dynamic([@"msagent_", @"MSSE-", @"postex_", @"status_", @"mypipe-f", @"mypipe-h",@"ntsvcs_",@"scerpc_", @"mojo.5688.8052."]);

Let Binding: CobaltStrikeMallable used in Stage 5

let CobaltStrikeMallable = dynamic([@"win_svc", @"ntsvcs", @"scerpc", @"status_", @"SearchTextHarvester", @"DserNamePipe",@"wkssvc_",@"scerpc_", @"spoolss_",@"CatalogChangeListener",@"fullduplex_",@"demoagent_",@"PGMessagePipe",@"MsFteWds",@"postex_ssh_",@"windows.update.manager",@"\f4c3",@"\f53f",@"halfduplex_"]);

Stage 1: source

DeviceEvents

Stage 2: where

| where Timestamp >= ago(timeframe)

Stage 3: where

| where ActionType == "NamedPipeEvent"

Stage 4: extend (5 consecutive steps)

| extend AdditionalFields=parse_json(AdditionalFields)
| extend ThreadId=tostring(AdditionalFields.ThreadId)
| extend PipeName=tostring(AdditionalFields.PipeName)
| extend InitiatingPID=tostring(InitiatingProcessId)
| extend InitiatingParentPID=tostring(InitiatingProcessParentId)

Stage 5: where

| where PipeName has_any (CobaltStrikeDefaults) or
      (PipeName matches regex @"\\mojo\.\d+\.\d+\." and not(PipeName matches regex @"\\mojo\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
      (PipeName matches regex @"\\(edge|chrome)\.sync\.\d+\.\d+\." and not(PipeName matches regex @"\\(edge|chrome|edge\.sync|chrome\.sync)\.\d+\.\d+\.\d+$" or PipeName has InitiatingPID or PipeName has InitiatingParentPID or PipeName has ThreadId)) or
      (PipeName matches regex @"\\PSHost\.\d+\." and not(PipeName matches regex @"\\PSHost\.\d+\.\d+\." or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
      (PipeName matches regex @"\\crashpad_" and not(PipeName matches regex @"\\crashpad_\d+_[A-Z]+" or PipeName has InitiatingPID or PipeName has InitiatingParentPID)) or
      (PipeName matches regex @"\\cubeb-pipe-" and not(PipeName matches regex @"\\cubeb-pipe-\d+_[0-9]{1-3}+" or PipeName has InitiatingPID)) or
      (PipeName has_any (CobaltStrikeMallable) and PipeName matches regex @"[a-fA-F0-9]{2,10}$") or
      (PipeName matches regex @"\\pipe\\[0-9a-f]{7,10}" or PipeName matches regex @"\\pipe\\[0-9a-f]{8}")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • NamedPipeEvent corpus 2 (kusto 2)
field:"ActionType" kind:eq value:"NamedPipeEvent"
PipeNamematch
  • CatalogChangeListener transforms: term
  • DserNamePipe transforms: term
  • MSSE- transforms: term
  • MsFteWds transforms: term
  • PGMessagePipe transforms: term
  • SearchTextHarvester transforms: term
  • \f4c3 transforms: term
  • \f53f transforms: term
  • demoagent_ transforms: term
  • fullduplex_ transforms: term
  • halfduplex_ transforms: term
  • mojo.5688.8052. transforms: term
  • msagent_ transforms: term
  • mypipe-f transforms: term
  • mypipe-h transforms: term
  • ntsvcs transforms: term
  • ntsvcs_ transforms: term
  • postex_ transforms: term
  • postex_ssh_ transforms: term
  • scerpc transforms: term
  • scerpc_ transforms: term
  • spoolss_ transforms: term
  • status_ transforms: term
  • win_svc transforms: term
  • windows.update.manager transforms: term
  • wkssvc_ transforms: term
field:"PipeName" kind:match
PipeNameregex_match
  • [a-fA-F0-9]{2,10}$
  • \(edge|chrome).sync.\d+.\d+.
  • \PSHost.\d+.
  • \crashpad_
  • \cubeb-pipe-
  • \mojo.\d+.\d+.
  • \pipe\[0-9a-f]{7,10}
  • \pipe\[0-9a-f]{8}
field:"PipeName" kind:regex_match

Output Fields

These fields are emitted when the rule matches.

FieldSource
AdditionalFieldsextend
ThreadIdextend
PipeNameextend
InitiatingPIDextend
InitiatingParentPIDextend

Suspicious Sign In by Entra ID Connect Sync Account

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

'This query looks for sign ins by the Microsoft Entra ID Connect Sync account to Azure where properties about the logon are anomalous. This query uses Microsoft Sentinel's UEBA features to detect these suspicious properties. A threat actor may attempt to steal the Sync account credentials and use them to access Azure resources. This alert should be reviewed to ensure that the log in came was from a legitimate source.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2cd8b3d5-c9e0-4be3-80f7-0469d511c3f6
name: Suspicious Sign In by Entra ID Connect Sync Account
description: |
  'This query looks for sign ins by the Microsoft Entra ID Connect Sync account to Azure where properties about the logon are anomalous.
  This query uses Microsoft Sentinel's UEBA features to detect these suspicious properties.
  A threat actor may attempt to steal the Sync account credentials and use them to access Azure resources. This alert should be 
  reviewed to ensure that the log in came was from a legitimate source.'
severity: Medium
requiredDataConnectors:
  - connectorId: BehaviorAnalytics
    dataTypes:
      - BehaviorAnalytics
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
query: |
  BehaviorAnalytics
  // User modification is expected from this account so focus on logons
  | where ActivityType =~ "LogOn"
  | where UserName startswith "Sync_" and UsersInsights.AccountDisplayName =~ "On-Premises Directory Synchronization Service Account"
  // Filter out this expected activity
  | where ActivityInsights.App !~ "Microsoft Azure Active Directory Connect"
  | where InvestigationPriority > 0
  | extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIPAddress
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DestinationDevice
alertDetailsOverride:
  alertDisplayNameFormat: Suspicious Sign In by AAD Connect Sync Account {{UserPrincipalName}} from {{SourceIPAddress}}
  alertDescriptionFormat: |
    This query looks for sign ins by the Azure AD Connect Sync account to Azure where properties about the logon are anomalous.
    This query uses Microsoft Sentinel's UEBA features to detect these suspicious properties.
    A threat actor may attempt to steal the Sync account credentials and use them to access Azure resources. This alert should be 
    reviewed to ensure that the log in came was from a legitimate source.
    In this case {{UserPrincipalName}} logged in from {{SourceIPAddress}}.
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Community
    support:
        tier: Community
    categories:
        domains: [ "Identity", "Security - Threat Protection" ]

Stages and Predicates

Stage 1: source

BehaviorAnalytics

Stage 2: where

| where ActivityType =~ "LogOn"

Stage 3: where

| where UserName startswith "Sync_" and UsersInsights.AccountDisplayName =~ "On-Premises Directory Synchronization Service Account"

Stage 4: where

| where ActivityInsights.App !~ "Microsoft Azure Active Directory Connect"

Stage 5: where

| where InvestigationPriority > 0

Stage 6: extend

| extend Name = split(UserPrincipalName, "@")[0], UPNSuffix = split(UserPrincipalName, "@")[1]

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Nameextend
UPNSuffixextend

Tailscale Premium: Large outbound transfer over tailnet

#
Status
available
Severity
medium
Time window
1h
Group by
Dst, DstNodeName, DstUser, NodeId, Proto, Src, SrcNodeName, SrcUser
Source
github.com/Azure/Azure-Sentinel

Identifies when a single src-dst pair transfers more than 100 MB over the tailnet within a 1-hour window. Large bursts can indicate data staging, exfiltration, or a misconfigured backup. Requires Tailscale Premium or Enterprise.

MITRE ATT&CK Coverage

Rule Body

id: d2e3f4a5-2b3c-4d5e-6f7a-8b9c0d1e2f3a
name: "Tailscale Premium: Large outbound transfer over tailnet"
description: |
  Identifies when a single src-dst pair transfers more than 100 MB over the tailnet within a 1-hour window. Large bursts can indicate data staging, exfiltration, or a misconfigured backup. Requires Tailscale Premium or Enterprise.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: TailscalePremiumCCF
    dataTypes:
      - Tailscale_Network_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Exfiltration
  - Collection
relevantTechniques:
  - T1041
  - T1020
query: |
  let bytesThreshold = 100 * 1024 * 1024;
  Tailscale_Network_CL
  | where TimeGenerated > ago(1h)
  | where HasVirtualTraffic
  | mv-expand t = VirtualTraffic
  | extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), Bytes = tolong(t.txBytes) + tolong(t.rxBytes), Pkts = tolong(t.txPkts) + tolong(t.rxPkts)
  | summarize TotalBytes = sum(Bytes), TotalPackets = sum(Pkts) by NodeId, SrcNodeName, SrcUser, DstNodeName, DstUser, Src, Dst, Proto
  | where TotalBytes > bytesThreshold
  | extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)
  | order by TotalBytes desc
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcNodeName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DstNodeName
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: SrcUser
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Parameters

let bytesThreshold = 100 * 1024 * 1024;

Stage 1: source

Tailscale_Network_CL

Stage 2: where

| where TimeGenerated > ago(1h)

Stage 3: where

| where HasVirtualTraffic

Stage 4: mv-expand

| mv-expand t = VirtualTraffic

Stage 5: extend

| extend Src = tostring(t.src), Dst = tostring(t.dst), Proto = toint(t.proto), Bytes = tolong(t.txBytes) + tolong(t.rxBytes), Pkts = tolong(t.txPkts) + tolong(t.rxPkts)

Stage 6: summarize

| summarize TotalBytes = sum(Bytes), TotalPackets = sum(Pkts) by NodeId, SrcNodeName, SrcUser, DstNodeName, DstUser, Src, Dst, Proto

Stage 7: where

| where TotalBytes > bytesThreshold

Stage 8: extend

| extend TotalMB = round(TotalBytes / 1024.0 / 1024.0, 2)

Stage 9: sort

| order by TotalBytes desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
HasVirtualTrafficeq
  • true transforms: boolean
field:"HasVirtualTraffic" kind:eq value:"true"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Dstsummarize
DstNodeNamesummarize
DstUsersummarize
NodeIdsummarize
Protosummarize
Srcsummarize
SrcNodeNamesummarize
SrcUsersummarize
TotalBytessummarize
TotalPacketssummarize
TotalMBextend

TI Map URL Entity to SecurityAlert Data

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

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

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: f30a47c1-65fb-42b1-a7f4-00941c12550b
name: TI Map URL Entity to SecurityAlert Data
description: |
  'This query identifies any URL indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in SecurityAlert data.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftCloudAppSecurity
    dataTypes:
      - SecurityAlert
  - connectorId: AzureSecurityCenter
    dataTypes:
      - SecurityAlert
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let URLRegex = "((https?|ftp|ldap|wss?|file):\\/\\/(([\\:\\%\\w\\_\\-]+(\\.|@))*((xn--)?[a-zA-Z0-9\\-]+\\.)+(xn--[a-z0-9]+|[A-Za-z]+)|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{0,3})[.,:\\w@?^=%&\\/~+#-]*[\\w@?^=%&\\/~+#-])";
  let SecurityEvents = materialize(SecurityAlert
    | where TimeGenerated >= ago(dt_lookBack)
    | extend MSTI = case(AlertName has "TI map" and VendorName == "Microsoft" and ProductName == 'Azure Sentinel', true, false)
    | where MSTI == false
    // Extract URL from JSON data
    | mv-expand parse_json(Entities)
    | where isnotempty(Entities.Url) or isnotempty(Entities.Urls)
    | extend Url = coalesce(Entities.Url, Entities.Urls)
    | mv-expand Url
    | extend Url = tolower(Url)
    // Extract hostname from JSON data for entity mapping
    | extend Compromised_Host = tostring(parse_json(ExtendedProperties).["Compromised Host"])
    | extend Alert_TimeGenerated = TimeGenerated);
  let EventUrls = materialize(SecurityEvents | distinct Url | summarize make_list(Url));
  ThreatIntelligenceIndicator
  | where isnotempty(Url)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend Url = tolower(Url)
  | where tolower(Url) in (EventUrls)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;" 
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (SecurityEvents) on Url
  | where Alert_TimeGenerated < ExpirationDateTime
  | summarize Alert_TimeGenerated = arg_max(Alert_TimeGenerated, *) by IndicatorId, AlertName
  | project timestamp = Alert_TimeGenerated, ActivityGroupNames, IndicatorId, ThreatType, ExpirationDateTime, ConfidenceScore, AlertName, AlertSeverity, Description, Url, Compromised_Host
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Compromised_Host
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.2.8
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: URLRegex

let URLRegex = "((https?|ftp|ldap|wss?|file):\\/\\/(([\\:\\%\\w\\_\\-]+(\\.|@))*((xn--)?[a-zA-Z0-9\\-]+\\.)+(xn--[a-z0-9]+|[A-Za-z]+)|\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{0,3})[.,:\\w@?^=%&\\/~+#-]*[\\w@?^=%&\\/~+#-])";

Let Binding: SecurityEvents used in Stage 9

let SecurityEvents = materialize(SecurityAlert
  | where TimeGenerated >= ago(dt_lookBack)
  | extend MSTI = case(AlertName has "TI map" and VendorName == "Microsoft" and ProductName == 'Azure Sentinel', true, false)
  | where MSTI == false
  | mv-expand parse_json(Entities)
  | where isnotempty(Entities.Url) or isnotempty(Entities.Urls)
  | extend Url = coalesce(Entities.Url, Entities.Urls)
  | mv-expand Url
  | extend Url = tolower(Url)
  | extend Compromised_Host = tostring(parse_json(ExtendedProperties).["Compromised Host"])
  | extend Alert_TimeGenerated = TimeGenerated);

Let Binding: EventUrls used in Stage 5

let EventUrls = materialize(SecurityEvents | distinct Url | summarize make_list(Url));

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(Url)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: extend

| extend Url = tolower(Url)

Stage 5: where

| where tolower(Url) in (EventUrls)

Stage 6: summarize

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

Stage 7: where

| where Active == true and ExpirationDateTime > now()

Stage 8: where

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

Stage 9: join

| join kind=innerunique (SecurityEvents) on Url

Stage 10: where

where Alert_TimeGenerated < ExpirationDateTime

Stage 11: summarize

summarize Alert_TimeGenerated by IndicatorId, AlertName

Stage 12: project

project ActivityGroupNames, AlertName, AlertSeverity, Compromised_Host, ConfidenceScore, Description, ExpirationDateTime, IndicatorId, ThreatType, Url, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActivityGroupNamesproject
AlertNameproject
AlertSeverityproject
Compromised_Hostproject
ConfidenceScoreproject
Descriptionproject
ExpirationDateTimeproject
IndicatorIdproject
ThreatTypeproject
Urlproject
timestampproject

Vaikora - High severity AI agent action

#

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

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

Identifies high or critical severity AI agent actions ingested from Vaikora in the last 6 hours.

MITRE ATT&CK Coverage

TacticTechniques
Initial AccessNo specific technique
ExecutionNo specific technique
PersistenceNo specific technique
StealthNo specific technique
Credential AccessNo specific technique
DiscoveryNo specific technique
Lateral MovementNo specific technique
CollectionNo specific technique
Command & ControlNo specific technique
ExfiltrationNo specific technique
ImpactNo specific technique

Rule Body

id: ac3ec787-fd49-4e93-88cc-aaa9b31061ac
name: Vaikora - High severity AI agent action
description: |
  Identifies high or critical severity AI agent actions ingested from Vaikora in the last 6 hours.
severity: High
requiredDataConnectors:
  - connectorId: VaikoraSecurityCenter
    dataTypes:
      - Vaikora_AgentSignals_CL
queryFrequency: 6h
queryPeriod: 6h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
  - Execution
  - Persistence
  - DefenseEvasion
  - CredentialAccess
  - Discovery
  - LateralMovement
  - Collection
  - CommandAndControl
  - Exfiltration
  - Impact
relevantTechniques: []
query: |
  Vaikora_AgentSignals_CL
  | where TimeGenerated >= ago(6h)
  | where severity_s in ("high", "critical")
  | extend
      ActionId     = action_id_s,
      AgentId      = agent_id_s,
      ActionType   = action_type_s,
      Severity     = severity_s,
      Status       = status_s,
      IsAnomaly    = is_anomaly_b,
      AnomalyScore = anomaly_score_d
  | project TimeGenerated, ActionId, AgentId, ActionType, Severity, Status, IsAnomaly, AnomalyScore
  | order by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AgentId
alertDetailsOverride:
  alertDisplayNameFormat: "Vaikora {{severity_s}} agent action: {{action_type_s}}"
  alertDescriptionFormat: "Vaikora detected a {{severity_s}} severity action on agent {{agent_id_s}}."
  alertSeverityColumnName: severity_s
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

Vaikora_AgentSignals_CL

Stage 2: where

| where TimeGenerated >= ago(6h)

Stage 3: where

| where severity_s in ("high", "critical")

Stage 4: extend

| extend
    ActionId     = action_id_s,
    AgentId      = agent_id_s,
    ActionType   = action_type_s,
    Severity     = severity_s,
    Status       = status_s,
    IsAnomaly    = is_anomaly_b,
    AnomalyScore = anomaly_score_d

Stage 5: project

| project TimeGenerated, ActionId, AgentId, ActionType, Severity, Status, IsAnomaly, AnomalyScore

Stage 6: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
severity_sin
  • critical
  • high
field:"okta::severity" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActionIdproject
ActionTypeproject
AgentIdproject
AnomalyScoreproject
IsAnomalyproject
Severityproject
Statusproject
TimeGeneratedproject

Valimail Enforce - Email Authentication Key Deleted

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

This query searches for deletion of SPF delegations or DKIM keys, which are medium-severity events that could degrade email authentication posture for a domain.

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: 483078c6-d029-40f3-931a-30af0032008b
name: Valimail Enforce - Email Authentication Key Deleted
description: |
  This query searches for deletion of SPF delegations or DKIM keys, which are medium-severity events
  that could degrade email authentication posture for a domain.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: ValimailEnforce
    dataTypes:
      - ValimailEnforceEvents_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
relevantTechniques:
  - T1562
query: |
  ValimailEnforceEvents_CL
  | where EventSeverity == "Medium"
  | where EventCategory in ("SPFConfiguration", "DKIMConfiguration")
  | where IsHighValueEvent == true
  | summarize
      EventCount = count(),
      FirstSeen = min(PerformedAt),
      LastSeen = max(PerformedAt),
      AffectedDomains = make_set(Subject),
      Actions = make_set(EventType)
    by User, EventCategory
  | extend
      AccountName = tostring(split(User, "@")[0]),
      AccountDomain = tostring(split(User, "@")[1]),
      DomainName = tostring(AffectedDomains[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountDomain
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: DomainName
alertDetailsOverride:
  alertDisplayNameFormat: "{{EventCategory}} key deleted on {{EventCount}} domain(s) by {{User}}"
  alertDescriptionFormat: |
    User '{{User}}' deleted one or more {{EventCategory}} records affecting
    domains: {{AffectedDomains}}. This may weaken email authentication posture.
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1d
    matchingMethod: Selected
    groupByEntities:
      - Account
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

ValimailEnforceEvents_CL

Stage 2: where

| where EventSeverity == "Medium"

Stage 3: where

| where EventCategory in ("SPFConfiguration", "DKIMConfiguration")

Stage 4: where

| where IsHighValueEvent == true

Stage 5: summarize

| summarize
    EventCount = count(),
    FirstSeen = min(PerformedAt),
    LastSeen = max(PerformedAt),
    AffectedDomains = make_set(Subject),
    Actions = make_set(EventType)
  by User, EventCategory

Stage 6: extend

| extend
    AccountName = tostring(split(User, "@")[0]),
    AccountDomain = tostring(split(User, "@")[1]),
    DomainName = tostring(AffectedDomains[0])

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionssummarize
AffectedDomainssummarize
EventCategorysummarize
EventCountsummarize
FirstSeensummarize
LastSeensummarize
Usersummarize
AccountDomainextend
AccountNameextend
DomainNameextend

VMware Edge Cloud Orchestrator - New LAN-Side Client Device Detected

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

This analytics rule creates notifications of newly connected devices. These clients are connected to the LAN interface of the Edge.

Rule Body

id: 69c0644f-4ad5-41b6-9e09-a94c072ab80e
name: VMware Edge Cloud Orchestrator - New LAN-Side Client Device Detected
version: 1.0.0
kind: Scheduled
description: This analytics rule creates notifications of newly connected devices. These clients are connected to the LAN interface of the Edge.
severity: Informational
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
requiredDataConnectors:
  - connectorId: VMwareSDWAN
    dataTypes:
      - SDWAN
query: |+
  VMware_VECO_EventLogs_CL
  | extend details = todynamic(detail)
  | evaluate bag_unpack(details)
  | where event == "EDGE_NEW_DEVICE"

suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDynamicProperties: []
customDetails:
  Client_MAC_Address: client_mac
  DHCP_Parameter_List: dhcp_param_list
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: hostname
  - identifier: OSFamily
    columnName: os_description
  - identifier: OSVersion
    columnName: os_version
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: client_ipv4addr
suppressionDuration: 5h

Stages and Predicates

Stage 1: source

VMware_VECO_EventLogs_CL

Stage 2: extend

| extend details = todynamic(detail)

Stage 3: evaluate

| evaluate bag_unpack(details)

Stage 4: where

| where event == "EDGE_NEW_DEVICE"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
eventeq
  • EDGE_NEW_DEVICE
field:"event" kind:eq value:"EDGE_NEW_DEVICE"

Output Fields

These fields are emitted when the rule matches.

FieldSource
detailsextend

Windows Binaries Executed from Non-Default Directory

#
Status
available
Severity
medium
Time window
1h
Group by
CommandLine, Computer, EventID, NewProcessName, Process, SubjectUserName
Source
github.com/Azure/Azure-Sentinel

The query detects Windows binaries, that can be executed from a non-default directory (e.g. C:\Windows, C:\Windows\System32 etc.). Ref: https://lolbas-project.github.io/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 15049017-527f-4d3b-b011-b0e99e68ef45
name: Windows Binaries Executed from Non-Default Directory
description: |
   'The query detects Windows binaries, that can be executed from a non-default directory (e.g. C:\Windows\, C:\Windows\System32 etc.). 
   Ref: https://lolbas-project.github.io/'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
   let procList = externaldata(Process:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Microsoft_Lolbas_Execution_Binaries.csv"] with (format="csv", ignoreFirstRecord=True);
   SecurityEvent
   | where EventID == 4688 and Process has_any (procList) and not (NewProcessName has ("C:\\Windows\\"))
   | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, SubjectUserName, NewProcessName, Process, CommandLine
   | extend Name=tostring(split(SubjectUserName, "@")[0]), UPNSuffix=tostring(split(SubjectUserName, "@")[1])
   | extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: CommandLine
version: 1.0.4
kind: Scheduled

Stages and Predicates

Let Binding: procList used in Stage 2

let procList = externaldata(Process:string) [@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Microsoft_Lolbas_Execution_Binaries.csv"] with (format="csv", ignoreFirstRecord=True);

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID == 4688 and Process has_any (procList) and not (NewProcessName has ("C:\\Windows\\"))

Stage 3: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, SubjectUserName, NewProcessName, Process, CommandLine

Stage 4: extend

| extend Name=tostring(split(SubjectUserName, "@")[0]), UPNSuffix=tostring(split(SubjectUserName, "@")[1])

Stage 5: extend

| extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4688 corpus 23 (kusto 23)
field:"EventID" kind:eq value:"4688"
Processmatch
  • procList transforms: term
field:"Process" kind:match value:"procList"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
NewProcessNamematchC:\Windows\excludes:NewProcessName field:"NewProcessName" value:"C:\Windows\"

Output Fields

These fields are emitted when the rule matches.

FieldSource
CommandLinesummarize
Computersummarize
EndTimesummarize
EventIDsummarize
NewProcessNamesummarize
Processsummarize
StartTimesummarize
SubjectUserNamesummarize
Nameextend
UPNSuffixextend
DnsDomainextend
HostNameextend

Back to all Kusto rules