Detection rules › Kusto

Kusto rules: 2a

AWS role with shadow admin privileges

#

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

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

The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2526079b-3355-4756-a2d1-21e9cd957261
name: AWS role with shadow admin privileges
kind: Scheduled
description: The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.
severity: High
status: Available
queryFrequency: 30m
queryPeriod: 30m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |-
  Authomize_v2_CL
  | where ingestion_time() >= ago(30m)
  | extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s 
  | where Policy has "AWS role with shadow admin privileges"
  | project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: URL
requiredDataConnectors:
- connectorId: Authomize
  dataTypes: [ "Authomize_v2_CL" ]
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
eventGroupingSettings:
  aggregationKind: SingleAlert
alertDetailsOverride:
  alertnameFormat: Alert from Authomize - AWS role with shadow admin privileges
  alertDescriptionFormat: |
    IaaS shadow admin detected. The policy detect the creation of new AWS roles with shadow admin privileges. The policy configuration allows limiting the policy to specific accounts.
  alertSeverity: Severity
  alertTactics: Tactics
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: URL
customDetails:
  AuthomizeEventID: EventID
  EventName: Policy
  EventDescription: Description
  EventRecommendation: Recommendation
  ReferencedURL: URL
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.2

Stages and Predicates

Stage 1: source

Authomize_v2_CL

Stage 2: where

| where ingestion_time() >= ago(30m)

Stage 3: extend

| extend EventID = id_s, Policy = policy_name_s, Severity = severity_s,Description = description_s,Recommendation = recommendation_s,URL = url_s,Tactics = tactics_s

Stage 4: where

| where Policy has "AWS role with shadow admin privileges"

Stage 5: project

| project  EventID, Policy, Severity, Description, Recommendation, URL, Category, Tactics

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Policymatch
  • AWS role with shadow admin privileges transforms: term
field:"policy_name_s" kind:match value:"AWS role with shadow admin privileges"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Categoryproject
Descriptionproject
EventIDproject
Policyproject
Recommendationproject
Severityproject
Tacticsproject
URLproject

Cayosoft Guardian - Core Identity and Infrastructure Threats

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

Creates Microsoft Sentinel incidents for identity and infrastructure security threats reported by Cayosoft Guardian across on-premises Active Directory and Microsoft Entra ID. This includes threats related to accounts, service accounts, computers, security groups, and trusted domains. Incident severity is determined dynamically based on the severity of the detected threat. This rule complements the Cloud Application Security Threats rule, which covers cloud application and service principal threat alerts not included in this rule. Alerts with the same Cayosoft ThreatId are grouped into a single incident within a 1-day lookback window. Each alert includes a link to the Cayosoft Threat Directory for guidance on remediation.

Rule Body

id: 4720d7a5-6845-4ce4-aa45-79334e1a1176
name: Cayosoft Guardian - Core Identity and Infrastructure Threats
description: |
  Creates Microsoft Sentinel incidents for identity and infrastructure security threats reported by Cayosoft Guardian across on-premises Active Directory and Microsoft Entra ID. This includes threats related to accounts, service accounts, computers, security groups, and trusted domains. Incident severity is determined dynamically based on the severity of the detected threat.

  This rule complements the Cloud Application Security Threats rule, which covers cloud application and service principal threat alerts not included in this rule.

  Alerts with the same Cayosoft ThreatId are grouped into a single incident within a 1-day lookback window. Each alert includes a link to the Cayosoft Threat Directory for guidance on remediation.
contentId: CayosoftGuardianMainAlerts
status: Available
requiredDataConnectors:
- connectorId: CayosoftGuardianConnector
  dataTypes:
  - CayosoftThreatAlerts_CL
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
suppressionDuration: 1h
suppressionEnabled: false
severity: Medium
tactics: []
relevantTechniques: []
query: |
  // ATTENTION: ruleFrequency must match the "queryFrequency" value in the rule's scheduling settings!
  // ATTENTION: lookbackPeriod must match the "queryPeriod" value in the rule's scheduling settings!
  // If you change one, change both, in every location, or you will reintroduce duplicate alerts.
  // Pattern reference: https://learn.microsoft.com/en-us/azure/sentinel/ingestion-delay
  let ruleFrequency = 1h;
  let lookbackPeriod = 1d;
  let window_end = bin(now(), ruleFrequency);      
  let window_start = window_end - ruleFrequency;   
  CayosoftThreatAlerts_CL
  | where TimeGenerated >= ago(lookbackPeriod)
  | where ingestion_time() > window_start and ingestion_time() <= window_end
  | extend ParsedCategories = todynamic(Categories)
  | extend TargetObjectTypeNormalized = tostring(TargetObjectType)
  | extend TargetLocationNormalized = tostring(TargetLocation)
  | mv-apply Cat = ParsedCategories on (
      summarize Values = make_list(tostring(Cat.value)) by Key = iff(tostring(Cat.name) startswith 'MITRE ATT', 'MITRE ATT&CK', tostring(Cat.name))
  | summarize CleanCategories = make_bag(pack(Key, strcat_array(Values, ', ')))
      )
  | extend Tactics = iff(isnotempty(CleanCategories['Attack tactics']), split(replace_string(tostring(CleanCategories['Attack tactics']), ' ', ''), ','), dynamic([]))
  | extend Techniques = iff(isnotempty(CleanCategories['MITRE ATT&CK']), extract_all(@"(T\d{4})", tostring(CleanCategories['MITRE ATT&CK'])), dynamic([]))
  | extend ExtractedDomain = tostring(split(TargetLocationNormalized, '/')[0])
  | extend IsEntra = TargetObjectTypeNormalized startswith 'microsoft.graph'
  | extend IsAD = TargetObjectTypeNormalized startswith 'microsoft.ad' or TargetObjectTypeNormalized has 'ServiceAccount'
  | extend IsGMSA = TargetObjectTypeNormalized contains 'ManagedServiceAccount'
  | extend ParsedUser = case(
      TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'ServiceAccount' or TargetObjectTypeNormalized has 'inetOrgPerson', TargetObjectName,
      extract("member\\s+'([^']+)'", 1, Subject)
      )
  | extend TargetAccountAadUserId = iff(IsEntra and TargetObjectTypeNormalized has 'user', TargetObjectId, dynamic(null))
  | extend TargetAccountName = case(
      IsEntra and TargetObjectTypeNormalized has 'user', iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
      IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectName,
      IsAD and TargetObjectTypeNormalized has 'directoryObject', TargetObjectName,
      isnotempty(ParsedUser), iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
      dynamic(null)
      )
  | extend TargetAccountObjectGuid = iff(IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectId, dynamic(null))
  | extend TargetHostName = iff(TargetObjectTypeNormalized has 'computer' or TargetObjectTypeNormalized has 'device', TargetObjectName, dynamic(null))
  | extend TargetHostDomain = iff(IsAD and TargetObjectTypeNormalized has 'computer', ExtractedDomain, dynamic(null))
  | extend TargetHostAzureID = iff(IsEntra and TargetObjectTypeNormalized has 'device', TargetObjectId, dynamic(null))
  | extend RawPath = iff(TargetLocationNormalized contains '/', substring(TargetLocationNormalized, indexof(TargetLocationNormalized, '/') + 1), '')
  // Convert domain
  | extend DomainDN = strcat('DC=', replace_string(ExtractedDomain, '.', ',DC='))
  // Smart path processing
  | extend PathDN = case(
      isempty(RawPath), '',
      RawPath has_any ('Builtin', 'Users', 'Computers', 'ForeignSecurityPrincipals', 'Managed Service Accounts'), strcat('CN=', replace_string(RawPath, '/', ',CN=')),
      strcat('OU=', replace_string(RawPath, '/', ',OU='))
  )
  // Assemble the final DistinguishedName and Name for the group
  | extend TargetGroupName = iff(TargetObjectTypeNormalized has 'group', TargetObjectName, dynamic(null))
  | extend TargetGroupDistinguishedName = iff(
      TargetObjectTypeNormalized has 'group', 
      strcat('CN=', TargetObjectName, iff(isnotempty(PathDN), strcat(',', PathDN), ''), ',', DomainDN), 
      dynamic(null)
  )
  | extend TargetGroupObjectGuid = iff(TargetObjectTypeNormalized has 'group', TargetObjectId, dynamic(null))
  | extend TargetDNSName = case(
      IsAD and TargetObjectTypeNormalized has 'trustedDomain', TargetObjectName,
      isnotempty(TargetLocationNormalized), ExtractedDomain,
      dynamic(null)
      )
  | extend ThreatDocUrl = tolower( strcat('https://www.cayosoft.com/threat-directory/', ThreatId, '/'))
  | extend AlertSeverity = case(
      Severity == 'high', 'High',
      Severity == 'medium', 'Medium',
      Severity == 'low', 'Low',
      Severity == 'informational', 'Informational',
      'Informational'
      )
  | project-away Categories, ParsedCategories
  | project-rename Categories = CleanCategories
  | where not(IsEntra and (TargetObjectType has 'application' or TargetObjectType has 'servicePrincipal'))
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: P1D
    matchingMethod: Selected
    groupByCustomDetails:
    - ThreatId
alertDetailsOverride:
  alertDisplayNameFormat: "{{Subject}}"
  alertDescriptionFormat: "Details: {{ThreatDocUrl}}"
  alertTacticsColumnName: Tactics
  alertSeverityColumnName: AlertSeverity
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: ThreatDocUrl
  - alertProperty: Techniques
    value: Techniques
customDetails:
  ThreatId: ThreatId
  Categories: Categories
  Evidence: Evidence
  Where: TargetLocation
  TargetName: TargetObjectName
  TargetType: TargetObjectType
  CreatedDateTime: CreatedDateTime
  RemediationComplex: RemediationComplexity
  ThreatDocUrl: ThreatDocUrl
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: AadUserId
    columnName: TargetAccountAadUserId
  - identifier: Name
    columnName: TargetAccountName
  - identifier: ObjectGuid
    columnName: TargetAccountObjectGuid
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: TargetHostName
  - identifier: NTDomain
    columnName: TargetHostDomain
  - identifier: AzureID
    columnName: TargetHostAzureID
- entityType: SecurityGroup
  fieldMappings:
  - identifier: DistinguishedName
    columnName: TargetGroupDistinguishedName
  - identifier: ObjectGuid
    columnName: TargetGroupObjectGuid
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: TargetDNSName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let ruleFrequency = 1h;
let lookbackPeriod = 1d;
let window_end = bin(now(), ruleFrequency);
let window_start = window_end - ruleFrequency;

Stage 1: source

CayosoftThreatAlerts_CL

Stage 2: where

| where TimeGenerated >= ago(lookbackPeriod)

Stage 3: where

| where ingestion_time() > window_start and ingestion_time() <= window_end

Stage 4: extend (3 consecutive steps)

| extend ParsedCategories = todynamic(Categories)
| extend TargetObjectTypeNormalized = tostring(TargetObjectType)
| extend TargetLocationNormalized = tostring(TargetLocation)

Stage 5: kusto:mv-apply

| mv-apply Cat = ParsedCategories on (
    summarize Values = make_list(tostring(Cat.value)) by Key = iff(tostring(Cat.name) startswith 'MITRE ATT', 'MITRE ATT&CK', tostring(Cat.name))
| summarize CleanCategories = make_bag(pack(Key, strcat_array(Values, ', ')))
    )

Stage 6: extend (22 consecutive steps)

| extend Tactics = iff(isnotempty(CleanCategories['Attack tactics']), split(replace_string(tostring(CleanCategories['Attack tactics']), ' ', ''), ','), dynamic([]))
| extend Techniques = iff(isnotempty(CleanCategories['MITRE ATT&CK']), extract_all(@"(T\d{4})", tostring(CleanCategories['MITRE ATT&CK'])), dynamic([]))
| extend ExtractedDomain = tostring(split(TargetLocationNormalized, '/')[0])
| extend IsEntra = TargetObjectTypeNormalized startswith 'microsoft.graph'
| extend IsAD = TargetObjectTypeNormalized startswith 'microsoft.ad' or TargetObjectTypeNormalized has 'ServiceAccount'
| extend IsGMSA = TargetObjectTypeNormalized contains 'ManagedServiceAccount'
| extend ParsedUser = case(
    TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'ServiceAccount' or TargetObjectTypeNormalized has 'inetOrgPerson', TargetObjectName,
    extract("member\\s+'([^']+)'", 1, Subject)
    )
| extend TargetAccountAadUserId = iff(IsEntra and TargetObjectTypeNormalized has 'user', TargetObjectId, dynamic(null))
| extend TargetAccountName = case(
    IsEntra and TargetObjectTypeNormalized has 'user', iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
    IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectName,
    IsAD and TargetObjectTypeNormalized has 'directoryObject', TargetObjectName,
    isnotempty(ParsedUser), iff(ParsedUser contains '@', tostring(split(ParsedUser, '@')[0]), ParsedUser),
    dynamic(null)
    )
| extend TargetAccountObjectGuid = iff(IsAD and (TargetObjectTypeNormalized has 'user' or TargetObjectTypeNormalized has 'foreignSecurityPrincipal' or IsGMSA), TargetObjectId, dynamic(null))
| extend TargetHostName = iff(TargetObjectTypeNormalized has 'computer' or TargetObjectTypeNormalized has 'device', TargetObjectName, dynamic(null))
| extend TargetHostDomain = iff(IsAD and TargetObjectTypeNormalized has 'computer', ExtractedDomain, dynamic(null))
| extend TargetHostAzureID = iff(IsEntra and TargetObjectTypeNormalized has 'device', TargetObjectId, dynamic(null))
| extend RawPath = iff(TargetLocationNormalized contains '/', substring(TargetLocationNormalized, indexof(TargetLocationNormalized, '/') + 1), '')
| extend DomainDN = strcat('DC=', replace_string(ExtractedDomain, '.', ',DC='))
| extend PathDN = case(
    isempty(RawPath), '',
    RawPath has_any ('Builtin', 'Users', 'Computers', 'ForeignSecurityPrincipals', 'Managed Service Accounts'), strcat('CN=', replace_string(RawPath, '/', ',CN=')),
    strcat('OU=', replace_string(RawPath, '/', ',OU='))
)
| extend TargetGroupName = iff(TargetObjectTypeNormalized has 'group', TargetObjectName, dynamic(null))
| extend TargetGroupDistinguishedName = iff(
    TargetObjectTypeNormalized has 'group', 
    strcat('CN=', TargetObjectName, iff(isnotempty(PathDN), strcat(',', PathDN), ''), ',', DomainDN), 
    dynamic(null)
)
| extend TargetGroupObjectGuid = iff(TargetObjectTypeNormalized has 'group', TargetObjectId, dynamic(null))
| extend TargetDNSName = case(
    IsAD and TargetObjectTypeNormalized has 'trustedDomain', TargetObjectName,
    isnotempty(TargetLocationNormalized), ExtractedDomain,
    dynamic(null)
    )
| extend ThreatDocUrl = tolower( strcat('https://www.cayosoft.com/threat-directory/', ThreatId, '/'))
| extend AlertSeverity = case(
    Severity == 'high', 'High',
    Severity == 'medium', 'Medium',
    Severity == 'low', 'Low',
    Severity == 'informational', 'Informational',
    'Informational'
    )
Tactics =
ifisnotempty(CleanCategories["Attack tactics"])split(replace_string(tostring(CleanCategories["Attack tactics"]), ' ', ''), ',')
elsedynamic([])

Stage 7: project-away

| project-away Categories, ParsedCategories

Stage 8: project-rename

| project-rename Categories = CleanCategories

Stage 9: where

| where not(IsEntra and (TargetObjectType has 'application' or TargetObjectType has 'servicePrincipal'))

Stage 10: summarize aggregation inside the mv-apply branch

summarize by Key

Stage 11: summarize aggregation inside the mv-apply branch

summarize

Exclusions

The rule actively suppresses these predicates.

Contrast ADR - Exploited Attack Event

#

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

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

'Detects successful exploitation of security vulnerabilities across all environments as identified by Contrast ADR. This rule captures confirmed exploited attacks that bypassed application security controls and require security team investigation.'

MITRE ATT&CK Coverage

Rule Body

id: ae4f67a6-0713-4a26-ae61-284e67b408c1
name: Contrast ADR - Exploited Attack Event
description: |
  'Detects successful exploitation of security vulnerabilities across all environments as identified by Contrast ADR. This rule captures confirmed exploited attacks that bypassed application security controls and require security team investigation.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: ContrastADRCCF
    dataTypes:
      - ContrastADRAttackEvents_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Execution
  - DefenseEvasion
  - LateralMovement
  - CommandAndControl
relevantTechniques:
  - T1190
  - T1059
  - T1055
  - T1210
  - T1008
query: |
  ContrastADRAttackEvents_CL
  | where result =~ "exploited"
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: sourceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: host_hostname
alertDetailsOverride:
  alertDisplayNameFormat: '{{result}} {{rule}} from {{sourceIp}} '
  alertDescriptionFormat: '{{result}}  on {{request_headers_referer}}  endpoint of {{application_name}} '
customDetails:
  AttackResult: result
  AttackRule: rule
  ApplicationName: application_name
  AttackedEndpoint: request_headers_referer
  Environment: environment
  TargetHost: host_hostname
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: Selected
    groupByEntities:
      - IP
      - Host
eventGroupingSettings:
  aggregationKind: AlertPerResult
kind: Scheduled
version: 1.0.1

Stages and Predicates

Stage 1: source

ContrastADRAttackEvents_CL

Stage 2: where

| where result =~ "exploited"

Indicators

These rows show field, operator, and value matches.

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

Cyble Vision Alerts Domain Watchlist

#

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

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

'Triggers when monitored domain DNS records change (A, NS, MX, TXT, SOA). DNS record changes may indicate misconfiguration, domain takeover attempts, or infrastructure shifts requiring review.'

MITRE ATT&CK Coverage

TacticTechniques
Resource Development

Rule Body

id: 0a54dc90-9a9d-4300-af21-feb5136e81da
name: Cyble Vision Alerts Domain Watchlist
description: |
  'Triggers when monitored domain DNS records change (A, NS, MX, TXT, SOA). DNS record changes may indicate misconfiguration, domain takeover attempts, or infrastructure shifts requiring review.'
severity: Low
status: Available
requiredDataConnectors:
- connectorId: CybleVisionAlerts
  dataTypes:
  - CybleVisionAlerts_CL
enabled: true
queryfrequency: 30m
queryPeriod: 30m
triggerOperator: GreaterThan
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics:
- ResourceDevelopment
relevantTechniques:
- T1584
query: |
  Alerts_domain_watchlist  
  | where Service == "domain_watchlist" 
  | extend MappedSeverity = Severity
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
  alertDetailsOverride:
  alertDisplayNameFormat: DNS Record Change Detected for {{DW_New_Domain}}
  alertDescriptionFormat: |
    Domain Watchlist Alert: DNS records for {{DW_New_Domain}} have changed. Updated fields {{DW_UpdatedFields}}. Review differences between old and new DNS records to determine if this is a legitimate update or a potential hijack attempt.
customDetails:
  MappedSeverity: Severity
  Status: Status
  AlertID: AlertID
  Service: Service
  DW_UpdatedFields: DW_UpdatedFields
  DW_Old_Domain: DW_Old_Domain
  DW_New_Domain: DW_New_Domain
entityMappings:
- entityType: DNS
  fieldMappings:
  - identifier: DomainName
    columnName: DW_New_Domain
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

Alerts_domain_watchlist

Stage 2: where

| where Service == "domain_watchlist"

Stage 3: extend

| extend MappedSeverity = Severity

Indicators

These rows show field, operator, and value matches.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
MappedSeverityextend

Dataverse - Login from IP not in the allow list

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

Identifies logons from IPv4 addresses not matching IPv4 subnets maintained on an allow list. This analytics rule uses the NetworkAddresses watchlist template.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 81c693fe-f6c4-4352-bc10-3526f6e22637
kind: Scheduled
name: Dataverse - Login from IP not in the allow list
description: Identifies logons from IPv4 addresses not matching IPv4 subnets maintained
  on an allow list. This analytics rule uses the NetworkAddresses watchlist template.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: Dataverse
    dataTypes:
      - DataverseActivity
queryFrequency: 1h
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
  - T1190
  - T1133
query: |
  // Use static IP address or CIDR list specified in the
  // NetworkAddresses watchlist template with tag "AllowDataverse"
  let allowed_networks = MSBizAppsNetworkAddresses()
      | where Tags has "AllowDataverse"
      | summarize by IPSubnet;
  let query_frequency = 1h;
  let watchlist_entries_count = toscalar (allowed_networks
      | summarize count());
  let dataverse_signin_activity = materialize(
      DataverseActivity
      | where watchlist_entries_count > 0
      | where TimeGenerated >= ago (query_frequency)
      | where Message == "UserSignIn" and isnotempty(ClientIp)
      | summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl
      );
  let authorized_ip_addresses = dataverse_signin_activity
      | evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);
  dataverse_signin_activity
  | join kind=leftanti(authorized_ip_addresses) on ClientIp
  | extend
      CloudAppId = int(32780),
      AccountName = tostring(split(UserId, '@')[0]),
      UPNSuffix = tostring(split(UserId, '@')[1])
  | project
      FirstEvent,
      UserId,
      ClientIp,
      InstanceUrl,
      CloudAppId,
      AccountName,
      UPNSuffix
eventGroupingSettings:
  aggregationKind: AlertPerResult
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: CloudAppId
      - identifier: InstanceName
        columnName: InstanceUrl
alertDetailsOverride:
  alertDisplayNameFormat: 'Dataverse - Login from IP not on the allow list in {{InstanceUrl}} '
  alertDescriptionFormat: Sign-in activity detected in {{InstanceUrl}} from an IP
    {{ClientIp}} not on the allow list.
version: 3.2.0

Stages and Predicates

Parameters

let query_frequency = 1h;

let dataverse_signin_activity is inlined into the numbered stages below.

Let Binding: allowed_networks

let allowed_networks = MSBizAppsNetworkAddresses()
    | where Tags has "AllowDataverse"
    | summarize by IPSubnet;

Let Binding: watchlist_entries_count used in Stage 2

let watchlist_entries_count = toscalar (allowed_networks
    | summarize count());

Let Binding: authorized_ip_addresses used in Stage 6

let authorized_ip_addresses = dataverse_signin_activity
    | evaluate ipv4_lookup(allowed_networks, ClientIp, IPSubnet);

Stages 1 to 5 define let dataverse_signin_activity as the rule's main pipeline source. Stages 6 to 8 use it.

Stage 1: source

DataverseActivity

Stage 2: where

| where watchlist_entries_count > 0

Stage 3: where

| where TimeGenerated >= ago (query_frequency)

Stage 4: where

| where Message == "UserSignIn" and isnotempty(ClientIp)

Stage 5: summarize

| summarize FirstEvent = arg_min(TimeGenerated, *) by UserId, ClientIp, InstanceUrl

Stage 6: join (negated)

join kind=leftanti (authorized_ip_addresses) on ClientIp

Stage 7: extend

extend AccountName, CloudAppId, UPNSuffix

Stage 8: project

project AccountName, ClientIp, CloudAppId, FirstEvent, InstanceUrl, UPNSuffix, UserId

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ClientIpis_not_null(no value, null check)excludes:ClientIp
MessageeqUserSignInexcludes:Message field:"Message" value:"UserSignIn"
watchlist_entries_countgt0excludes:watchlist_entries_count field:"watchlist_entries_count" value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNameproject
ClientIpproject
CloudAppIdproject
FirstEventproject
InstanceUrlproject
UPNSuffixproject
UserIdproject

Detect external user sending suspicious link to multiple users

#
Group by
$left.SenderObjectId, $right.AccountObjectId, SenderEmailAddress, TeamsMessageId, Url
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

An external sender suddenly sending the same link to multiple internal users, can indicate that external user being compromised and used for BEC Attacks. In these kind of attacks compromised accounts are used to send phishing links or attachments to users in business relationships.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

References

Telemetry Coverage

Rule Body

// External user sending same link to multiple users via Teams
let threshold = 5;
MessageEvents
| where TimeGenerated > ago(1d)
// Focus on chat messsages
| where ThreadType == "chat"
// Only return external users sending messages
| join kind=leftanti (
    IdentityInfo
    | where TimeGenerated > ago(14d)
    | distinct AccountObjectId
) on $left.SenderObjectId == $right.AccountObjectId
// Only flag messages with Teams Links
| join kind=inner MessageUrlInfo on TeamsMessageId
// Exclude teams file thumbnails
| where Url !~ "http://dummy.jpg/"
// Make a set of the chats a user sends a specific URL to
| summarize ChatSet = make_set(ThreadId) by SenderEmailAddress, Url
// Count the amount of chats
| extend ChatCount = array_length(ChatSet)
| where ChatCount > threshold

Stages and Predicates

Parameters

let threshold = 5;

Stage 1: source

MessageEvents

Stage 2: where

| where TimeGenerated > ago(1d)

Stage 3: where

| where ThreadType == "chat"

Stage 4: join (negated)

join kind=leftanti (IdentityInfo) on $left.SenderObjectId, $right.AccountObjectId

Stage 5: join

| join kind=inner MessageUrlInfo on TeamsMessageId

Stage 6: where

where Url !~ "http://dummy.jpg/"

Stage 7: summarize

summarize ChatSet by SenderEmailAddress, Url

Stage 8: extend

extend ChatCount

Stage 9: where

where ChatCount > 5

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ChatSetsummarize
SenderEmailAddresssummarize
Urlsummarize
ChatCountextend

Detect potential presence of a malicious file with a double extension (ASIM Web Session)

#
Status
available
Severity
medium
Time window
1h
Group by
DstHostname, DstIpAddr, DstPortNumber, FileWithdualextension, SrcHostname, SrcIpAddr, SrcUsername, Url
Source
github.com/Azure/Azure-Sentinel

'Double extension vulnerability is a significant concern in file uploads, as it can lead to various issues if an attacker successfully uploads a virus-infected file.'

MITRE ATT&CK Coverage

Rule Body

id: 6a71687f-00cf-44d3-93fc-8cbacc7b5615
name: Detect potential presence of a malicious file with a double extension (ASIM Web Session)
description: |
  'Double extension vulnerability is a significant concern in file uploads, as it can lead to various issues if an attacker successfully uploads a virus-infected file.'
severity: Medium
status: Available 
tags:
  - Schema: WebSession
    SchemaVersion: 0.2.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Persistence
  - CommandAndControl
relevantTechniques:
  - T1036
  - T1505
  - T1071
query: |
  let common_file_ext_list = dynamic([".txt", ".xlsx", ".doc", ".docx", ".csv", ".pdf", ".png", ".jpg", ".jpeg"]); // Add list of common files as per your environment
  _Im_WebSession (starttime=ago(1h), eventresult='Success')
  | where HttpRequestMethod in~ ("POST", "PUT") 
  | project
      Url,
      SrcIpAddr,
      SrcUsername,
      SrcHostname,
      DstIpAddr,
      DstPortNumber,
      DstHostname,
      TimeGenerated
  | extend requestedFileName=tostring(split(tostring(parse_url(Url)["Path"]), '/')[-1])
  | extend FileWithdualextension = extract(@'([\w-]+\.\w+\.\w+)$', 1, requestedFileName, typeof(string))
  | extend SecondExt = tostring(split(FileWithdualextension, '.')[-1])
  | where strcat('.', SecondExt) in~ (common_file_ext_list) // Second extension is mostly from the common files
  | summarize
      EventCount=count(),
      EventStartTime=min(TimeGenerated),
      EventEndTime=max(TimeGenerated)
      by
      SrcIpAddr,
      Url,
      FileWithdualextension,
      SrcUsername,
      SrcHostname,
      DstIpAddr,
      DstPortNumber,
      DstHostname
  | extend Name = iif(SrcUsername contains "@", tostring(split(SrcUsername,'@',0)[0]),SrcUsername), UPNSuffix = iif(SrcUsername contains "@",tostring(split(SrcUsername,'@',1)[0]),"")
entityMappings:
  - entityType: File
    fieldMappings:
      - identifier: Name
        columnName: FileWithdualextension
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: SrcHostname
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  EventCount: EventCount
  EventStartTime: EventStartTime
  EventEndTime: EventEndTime
  DstHostname: DstHostname
alertDetailsOverride:
  alertDisplayNameFormat: "User '{{SrcUsername}}' with IP address '{{SrcIpAddr}}' has been observed with posting potentially risky dual extension file"
  alertDescriptionFormat: "User posted file '{{FileWithdualextension}}' which potentially contain dual extensions. This type of activity could be malicious and performed to bypass file upload filters or security measures implemented by the application. Destination server name this request was targetted to - '{{DstHostname}}'"
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let common_file_ext_list = dynamic([".txt", ".xlsx", ".doc", ".docx", ".csv", ".pdf", ".png", ".jpg", ".jpeg"]);

Stage 1: source

_Im_WebSession (starttime=ago(1h), eventresult='Success')

Stage 2: where

| where HttpRequestMethod in~ ("POST", "PUT")

Stage 3: project

| project
    Url,
    SrcIpAddr,
    SrcUsername,
    SrcHostname,
    DstIpAddr,
    DstPortNumber,
    DstHostname,
    TimeGenerated

Stage 4: extend (3 consecutive steps)

| extend requestedFileName=tostring(split(tostring(parse_url(Url)["Path"]), '/')[-1])
| extend FileWithdualextension = extract(@'([\w-]+\.\w+\.\w+)$', 1, requestedFileName, typeof(string))
| extend SecondExt = tostring(split(FileWithdualextension, '.')[-1])

Stage 5: where

| where strcat('.', SecondExt) in~ (common_file_ext_list)

Stage 6: summarize

| summarize
    EventCount=count(),
    EventStartTime=min(TimeGenerated),
    EventEndTime=max(TimeGenerated)
    by
    SrcIpAddr,
    Url,
    FileWithdualextension,
    SrcUsername,
    SrcHostname,
    DstIpAddr,
    DstPortNumber,
    DstHostname

Stage 7: 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
DstHostnamesummarize
DstIpAddrsummarize
DstPortNumbersummarize
EventCountsummarize
EventEndTimesummarize
EventStartTimesummarize
FileWithdualextensionsummarize
SrcHostnamesummarize
SrcIpAddrsummarize
SrcUsernamesummarize
Urlsummarize
Nameextend
UPNSuffixextend

Excessive number of failed connections from a single source (ASIM Network Session schema)

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

This rule identifies a single source that generates an excessive amount of failed connections. Modify the threshold to change the sensitivity of the rule: the higher the threshold, the less sensitive is the rule and less incidents will be generated. This analytic rule uses ASIM and supports any built-in or custom source that supports the ASIM NetworkSession schema

MITRE ATT&CK Coverage

TacticTechniques
Impact

Telemetry Coverage

Rule Body

id: 4902eddb-34f7-44a8-ac94-8486366e9494
name: Excessive number of failed connections from a single source (ASIM Network Session schema)
description: |
  'This rule identifies a single source that generates an excessive amount of failed connections. Modify the threshold to change the sensitivity of the rule: the higher the threshold, the less sensitive is the rule and less incidents will be generated.
   This analytic rule uses [ASIM](https://aka.ms/AboutASIM) and supports any built-in or custom source that supports the ASIM NetworkSession schema'
severity: Medium
status: Available
tags:
  - ParentAlert: https://github.com/Azure/Azure-Sentinel/tree/master/Solutions/Sophos%20XG%20Firewall/Analytic%20Rules/ExcessiveAmountofDeniedConnectionsfromASingleSource.yaml
    version: 1.0.0
  - Schema: ASimNetworkSessions
    SchemaVersion: 0.2.4
requiredDataConnectors:
  - connectorId: AWSS3
    dataTypes:
      - AWSVPCFlow
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: Zscaler
    dataTypes:
      - CommonSecurityLog
  - connectorId: MicrosoftSysmonForLinux
    dataTypes:
      - Syslog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
  - connectorId: AzureFirewall
    dataTypes:
      - AzureDiagnostics
  - connectorId: AzureNSG
    dataTypes:
      - AzureDiagnostics
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: Corelight
    dataTypes:
      - Corelight_CL
  - connectorId: AIVectraStream
    dataTypes:
      - VectraStream
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoMeraki
    dataTypes:
      - Syslog
      - CiscoMerakiNativePoller

queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1499
query: |
  let threshold = 5000;
  _Im_NetworkSession(eventresult='Failure')
  | summarize Count=count() by SrcIpAddr, bin(TimeGenerated,5m)
  | where Count > threshold
  | extend timestamp = TimeGenerated, threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
customDetails:
  NumberOfDenies: Count

alertDetailsOverride:
  alertDisplayNameFormat: Excessive number of failed connections from {{SrcIpAddr}}
  alertDescriptionFormat: 'The client at address {{SrcIpAddr}} generated more than {{threshold}} failures over a 5 minutes time window, which may indicate malicious activity.'
version: 1.2.7
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 5000;

Stage 1: source

_Im_NetworkSession(eventresult='Failure')

Stage 2: summarize

| summarize Count=count() by SrcIpAddr, bin(TimeGenerated,5m)

Stage 3: where

| where Count > threshold

Stage 4: extend

| extend timestamp = TimeGenerated, threshold

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Countgt
  • 5000 transforms: number
field:"Count" kind:gt value:"5000"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Countsummarize
SrcIpAddrsummarize
thresholdextend
timestampextend

Google DNS - Multiple errors for source

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

'Detects multiple errors for the same source IP address.'

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Rule Body

id: 7e81a935-5e91-45a5-92fd-3b58c180513b
name: Google DNS - Multiple errors for source
description: |
  'Detects multiple errors for the same source IP address.'
severity: Medium
requiredDataConnectors:
  - connectorId: GCPDNSDataConnector
    dataTypes:
      - GCPCloudDNS
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1095
query: |
  let threshold = 10;
  let err = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);
  GCPCloudDNS
  | where EventResultDetails in~ (err)
  | summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)
  | where count_ > threshold
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;
let err = dynamic(['NXDOMAIN', 'SERVFAIL', 'REFUSED']);

Stage 1: source

GCPCloudDNS

Stage 2: where

| where EventResultDetails in~ (err)

Stage 3: summarize

| summarize count() by SrcIpAddr, bin(TimeGenerated, 10m)

Stage 4: where

| where count_ > threshold

Stage 5: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventResultDetailsin
  • NXDOMAIN
  • REFUSED
  • SERVFAIL
field:"EventResultDetails" kind:in
count_gt
  • 10 transforms: number
field:"count_" kind:gt value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SrcIpAddrsummarize
IPCustomEntityextend

NordPass - Manual invitation, suspension, or deletion

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

This will alert you when the user is manually invited, suspended, or deleted. !This rule should be enabled only by organizations that have User and Group Provisioning enabled.

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: 693c5217-e840-427f-9661-3fa0ef266040
name: NordPass - Manual invitation, suspension, or deletion
kind: Scheduled
version: 1.0.0
displayName: Manual invitation, suspension, or deletion
description: |
  This will alert you when the user is manually invited, suspended, or deleted.
  
  !This rule should be enabled only by organizations that have User and Group Provisioning enabled.
severity: Medium
query: |
  NordPassEventLogs_CL  
  | extend metadataParsed = parse_json(metadata)  
  | where (event_type == "invites" and action == "user_invited") or (event_type == "access" and action == "user_status_updated" and (metadataParsed.status == "suspended" or metadataParsed.status == "deleted"))  
  | extend TargetEmail = user_email
requiredDataConnectors:
  - connectorId: NordPass
    dataTypes:
      - NordPassEventLogs_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1098
incidentConfiguration:
  createIncident: false
entityMappings:
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: TargetEmail

Stages and Predicates

Stage 1: source

NordPassEventLogs_CL

Stage 2: extend

| extend metadataParsed = parse_json(metadata)

Stage 3: where

| where (event_type == "invites" and action == "user_invited") or (event_type == "access" and action == "user_status_updated" and (metadataParsed.status == "suspended" or metadataParsed.status == "deleted"))

Stage 4: extend

| extend TargetEmail = user_email

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
actioneq
  • user_invited
  • user_status_updated
field:"action" kind:eq
event_typeeq
  • access
  • invites
field:"event_type" kind:eq
metadataParsed.statuseq
  • deleted transforms: kusto_dynamic_path
  • suspended transforms: kusto_dynamic_path
field:"metadataParsed.status" kind:eq

Output Fields

These fields are emitted when the rule matches.

FieldSource
metadataParsedextend
TargetEmailextend

OracleDBAudit - SQL injection patterns

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

'Detects common known SQL injection patterns used in automated scripts.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: ab352f0d-7c55-4ab2-a22e-b1c2d995e193
name: OracleDBAudit - SQL injection patterns
description: |
  'Detects common known SQL injection patterns used in automated scripts.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1190
query: |
  OracleDatabaseAuditEvent
  | where isnotempty(DstUserName)
  | where Action has_any ("admin' --" ,"admin' #", "admin'/*", "0=1", "1=0", "1=1", "1=2", "' or 1=1--", "' or 1=1#", "' or 1=1/*", "') or '1'='1--", "') or ('1'='1--")
  | project SrcIpAddr, DstUserName, Action
  | extend AccountCustomEntity = DstUserName
  | extend IPCustomEntity = SrcIpAddr
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

OracleDatabaseAuditEvent

Stage 2: where

| where isnotempty(DstUserName)

Stage 3: where

| where Action has_any ("admin' --" ,"admin' #", "admin'/*", "0=1", "1=0", "1=1", "1=2", "' or 1=1--", "' or 1=1#", "' or 1=1/*", "') or '1'='1--", "') or ('1'='1--")

Stage 4: project

| project SrcIpAddr, DstUserName, Action

Stage 5: extend

| extend AccountCustomEntity = DstUserName

Stage 6: extend

| extend IPCustomEntity = SrcIpAddr

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionmatch
  • ' or 1=1# transforms: term
  • ' or 1=1-- transforms: term
  • ' or 1=1/* transforms: term
  • ') or '1'='1-- transforms: term
  • ') or ('1'='1-- transforms: term
  • 0=1 transforms: term
  • 1=0 transforms: term
  • 1=1 transforms: term
  • 1=2 transforms: term
  • admin' # transforms: term
  • admin' -- transforms: term
  • admin'/* transforms: term
field:"Action" kind:match
DstUserNameis_not_null
  • (no value, null check)
field:"DstUserName" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionproject
DstUserNameproject
SrcIpAddrproject
AccountCustomEntityextend
IPCustomEntityextend

Pathlock TDnR - SAP HTTP Webserver Events

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

Detects security-relevant events from the SAP HTTP webserver logfile, forwarded by Pathlock Threat Detection and Response. Anomalous HTTP activity may indicate web-based attacks, unauthorized access to SAP web services, or command-and-control communication.

MITRE ATT&CK Coverage

Rule Body

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

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "HTTP_LOG"

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

PowerShell without powershell.exe

#
Group by
InitiatingProcessSHA1
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query detects the use of PowerShell through "system.management.automation.dll" which is invoked by a process with a low global prevalence (i.e., fairly unique binary).

Known False Positives

  • Many legitimate programs will use the "system.management.automation.dll". This might lead to false-positives that require filtering.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let default_global_prevalence = 0;
let suspiciousProcs = materialize(
DeviceImageLoadEvents
| where FileName =~ "system.management.automation.dll" or FileName =~ "system.management.automation.ni.dll" and not(isempty(InitiatingProcessSHA1))
| summarize count() by InitiatingProcessSHA1
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(IsRootSignerMicrosoft, false) == false or  coalesce(IsCertificateValid, false) == false
| where (IsCertificateValid and coalesce(GlobalPrevalence,default_global_prevalence) < 200) or (not(coalesce(IsCertificateValid, false)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500));
DeviceImageLoadEvents
| where ingestion_time() >= ago(timeframe)
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| where InitiatingProcessSHA1 in~ ((suspiciousProcs | project InitiatingProcessSHA1)) and FileName startswith "System.Management.Automation"
| join kind=inner suspiciousProcs on InitiatingProcessSHA1
// Begin environment-specific filter.
// End environment-specific filter.

Stages and Predicates

Parameters

let timeframe = 2*1h;
let default_global_prevalence = 0;

Let Binding: suspiciousProcs used in Stages 1, 6

let suspiciousProcs = materialize(
DeviceImageLoadEvents
| where FileName =~ "system.management.automation.dll" or FileName =~ "system.management.automation.ni.dll" and not(isempty(InitiatingProcessSHA1))
| summarize count() by InitiatingProcessSHA1
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(ProfileAvailability =~ "Error")
| where coalesce(IsRootSignerMicrosoft, false) == false or  coalesce(IsCertificateValid, false) == false
| where (IsCertificateValid and coalesce(GlobalPrevalence,default_global_prevalence) < 200) or (not(coalesce(IsCertificateValid, false)) and coalesce(GlobalPrevalence,default_global_prevalence) < 500));

Stage 1: source

let suspiciousProcs

Stage 2: source

DeviceImageLoadEvents

Stage 3: where

where ...

Stage 4: extend

extend InitiatingProcessSHA1

Stage 5: where

where FileName startswith "System.Management.Automation"

Stage 6: join

join kind=inner (suspiciousProcs) on InitiatingProcessSHA1

Stage 7: summarize aggregation inside the join branch

summarize by InitiatingProcessSHA1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
FileNameeq
  • system.management.automation.dll
  • system.management.automation.ni.dll
field:"FileName" kind:eq
FileNamestarts_with
  • System.Management.Automation
field:"FileName" kind:starts_with value:"System.Management.Automation"
GlobalPrevalencelt
  • 200 transforms: coalesce_default:0
  • 500 transforms: coalesce_default:0
field:"GlobalPrevalence" kind:lt
IsCertificateValideq
  • false transforms: coalesce_default:false
  • true transforms: boolean
field:"IsCertificateValid" kind:eq
IsRootSignerMicrosofteq
  • false transforms: coalesce_default:false
field:"IsRootSignerMicrosoft" kind:eq value:"false"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
ProfileAvailabilityeqErrorexcludes:ProfileAvailability field:"ProfileAvailability" value:"Error"

Output Fields

These fields are emitted when the rule matches.

FieldSource
InitiatingProcessSHA1summarize

SAP LogServ - HANA DB - Audit Trail Policy Changes

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

Identifies changes for HANA DB audit trail policies. Source Action: Create / update existing audit policy in security definitions. Data Sources: SAP LogServ - HANA DB (Syslog)

MITRE ATT&CK Coverage

TacticTechniques
PersistenceNo specific technique
StealthNo specific technique
Lateral MovementNo specific technique

Rule Body

id: e8394afb-82a7-4718-8d31-cc57ad352fa8
kind: Scheduled
name: SAP LogServ - HANA DB - Audit Trail Policy Changes
description: |
  Identifies changes for HANA DB audit trail policies.
  
  Source Action: Create / update existing audit policy in security definitions.
  
  *Data Sources: SAP LogServ - HANA DB (Syslog)*
severity: High
status: Available
requiredDataConnectors:
  - connectorId: SAPLogServ
    dataTypes:
      - Syslog
queryFrequency: 10m
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - LateralMovement
  - DefenseEvasion
relevantTechniques: []
query: |
  let AuditTimeAgo = 60m;
  Syslog
  | where TimeGenerated >= ago(AuditTimeAgo)
  | where SourceSystem == "HANADB" and CollectorHostName == "SAPLogServ"
  | extend raw_split = split(SyslogMessage, ";")
  | extend
    event_timestamp__col_0 = tostring(raw_split[0]), 
    service_name__col_1 = tostring(raw_split[1]), 
    hostname__col_2 = tostring(raw_split[2]), 
    sid__col_3 = tostring(raw_split[3]), 
    instance_number__col_4 = tostring(raw_split[4]), 
    port_number__col_5 = tostring(raw_split[5]), 
    database_name__col_6 = tostring(raw_split[6]), 
    client_ip_address__col_7 = tostring(raw_split[7]), 
    client_name__col_8 = tostring(raw_split[8]), 
    client_process_id__col_9 = tostring(raw_split[9]), 
    client_port_number__col_10 = tostring(raw_split[10]), 
    policy_name__col_11 = tostring(raw_split[11]), 
    audit_level__col_12 = tostring(raw_split[12]), 
    audit_action__col_13 = tostring(raw_split[13]), 
    session_user__col_14 = tostring(raw_split[14]), 
    target_schema__col_15 = tostring(raw_split[15]), 
    target_object__col_16 = tostring(raw_split[16]), 
    privilege_name__col_17 = tostring(raw_split[17]), 
    grantable__col_18 = tostring(raw_split[18]), 
    role_name__col_19 = tostring(raw_split[19]), 
    target_principal__col_20 = tostring(raw_split[20]), 
    action_status__col_21 = tostring(raw_split[21]), 
    component__col_22 = tostring(raw_split[22]), 
    section__col_23 = tostring(raw_split[23]), 
    parameter__col_24 = tostring(raw_split[24]), 
    old_value__col_25 = tostring(raw_split[25]), 
    new_value__col_26 = tostring(raw_split[26]), 
    comment__col_27 = tostring(raw_split[27]), 
    executed_statement__col_28 = tostring(raw_split[28]), 
    session_id__col_29 = tostring(raw_split[29]), 
    application_user_name__col_30 = tostring(raw_split[30]), 
    role_schema_name__col_31 = tostring(raw_split[31]), 
    grantee_schema_name__col_32 = tostring(raw_split[32]), 
    origin_database_name__col_33 = tostring(raw_split[33]), 
    origin_user_name__col_34 = tostring(raw_split[34]), 
    xs_application_user_name__col_35 = tostring(raw_split[35]), 
    application_name__col_36 = tostring(raw_split[36]), 
    statement_user_name__col_37 = tostring(raw_split[37]), 
    create_time__col_38 = tostring(raw_split[38]), 
    xsa_message_ip__col_39 = tostring(raw_split[39]), 
    xsa_tenant__col_40 = tostring(raw_split[40]), 
    xsa_uuid__col_41 = tostring(raw_split[41]), 
    xsa_channel__col_42 = tostring(raw_split[42]), 
    xsa_attachment_id__col_43 = tostring(raw_split[43]), 
    xsa_attachment_name__col_44 = tostring(raw_split[44]), 
    xsa_organization_id__col_45 = tostring(raw_split[45]), 
    xsa_space_id__col_46 = tostring(raw_split[46]), 
    xsa_instance_id__col_47 = tostring(raw_split[47]), 
    xsa_binding_id__col_48 = tostring(raw_split[48]), 
    xsa_object__col_49 = tostring(raw_split[49]), 
    xsa_data_subject__col_50 = tostring(raw_split[50])
  | where audit_action__col_13 contains 'AUDIT POLICY' 
  | extend AlertRuleUniqueName = 'hanadb-audittrailpolicychanges-logserv'
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: CloudApplication
    fieldMappings:
      - identifier: AppId
        columnName: sid__col_3
      - identifier: InstanceName
        columnName: database_name__col_6
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: hostname__col_2
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: client_ip_address__col_7
alertDetailsOverride:
  alertDisplayNameFormat: 'SAP LogServ - HANA DB - Audit Trail Policy Change'
  alertDescriptionFormat: |
    {{comment__col_27}}
customDetails:
  SAP_User: session_user__col_14
version: 1.0.1

Stages and Predicates

Parameters

let AuditTimeAgo = 60m;

Stage 1: source

Syslog

Stage 2: where

| where TimeGenerated >= ago(AuditTimeAgo)

Stage 3: where

| where SourceSystem == "HANADB" and CollectorHostName == "SAPLogServ"

Stage 4: extend

| extend raw_split = split(SyslogMessage, ";")

Stage 5: extend

| extend
  event_timestamp__col_0 = tostring(raw_split[0]), 
  service_name__col_1 = tostring(raw_split[1]), 
  hostname__col_2 = tostring(raw_split[2]), 
  sid__col_3 = tostring(raw_split[3]), 
  instance_number__col_4 = tostring(raw_split[4]), 
  port_number__col_5 = tostring(raw_split[5]), 
  database_name__col_6 = tostring(raw_split[6]), 
  client_ip_address__col_7 = tostring(raw_split[7]), 
  client_name__col_8 = tostring(raw_split[8]), 
  client_process_id__col_9 = tostring(raw_split[9]), 
  client_port_number__col_10 = tostring(raw_split[10]), 
  policy_name__col_11 = tostring(raw_split[11]), 
  audit_level__col_12 = tostring(raw_split[12]), 
  audit_action__col_13 = tostring(raw_split[13]), 
  session_user__col_14 = tostring(raw_split[14]), 
  target_schema__col_15 = tostring(raw_split[15]), 
  target_object__col_16 = tostring(raw_split[16]), 
  privilege_name__col_17 = tostring(raw_split[17]), 
  grantable__col_18 = tostring(raw_split[18]), 
  role_name__col_19 = tostring(raw_split[19]), 
  target_principal__col_20 = tostring(raw_split[20]), 
  action_status__col_21 = tostring(raw_split[21]), 
  component__col_22 = tostring(raw_split[22]), 
  section__col_23 = tostring(raw_split[23]), 
  parameter__col_24 = tostring(raw_split[24]), 
  old_value__col_25 = tostring(raw_split[25]), 
  new_value__col_26 = tostring(raw_split[26]), 
  comment__col_27 = tostring(raw_split[27]), 
  executed_statement__col_28 = tostring(raw_split[28]), 
  session_id__col_29 = tostring(raw_split[29]), 
  application_user_name__col_30 = tostring(raw_split[30]), 
  role_schema_name__col_31 = tostring(raw_split[31]), 
  grantee_schema_name__col_32 = tostring(raw_split[32]), 
  origin_database_name__col_33 = tostring(raw_split[33]), 
  origin_user_name__col_34 = tostring(raw_split[34]), 
  xs_application_user_name__col_35 = tostring(raw_split[35]), 
  application_name__col_36 = tostring(raw_split[36]), 
  statement_user_name__col_37 = tostring(raw_split[37]), 
  create_time__col_38 = tostring(raw_split[38]), 
  xsa_message_ip__col_39 = tostring(raw_split[39]), 
  xsa_tenant__col_40 = tostring(raw_split[40]), 
  xsa_uuid__col_41 = tostring(raw_split[41]), 
  xsa_channel__col_42 = tostring(raw_split[42]), 
  xsa_attachment_id__col_43 = tostring(raw_split[43]), 
  xsa_attachment_name__col_44 = tostring(raw_split[44]), 
  xsa_organization_id__col_45 = tostring(raw_split[45]), 
  xsa_space_id__col_46 = tostring(raw_split[46]), 
  xsa_instance_id__col_47 = tostring(raw_split[47]), 
  xsa_binding_id__col_48 = tostring(raw_split[48]), 
  xsa_object__col_49 = tostring(raw_split[49]), 
  xsa_data_subject__col_50 = tostring(raw_split[50])

Stage 6: where

| where audit_action__col_13 contains 'AUDIT POLICY'

Stage 7: extend

| extend AlertRuleUniqueName = 'hanadb-audittrailpolicychanges-logserv'

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
raw_splitextend
action_status__col_21extend
application_name__col_36extend
application_user_name__col_30extend
audit_action__col_13extend
audit_level__col_12extend
client_ip_address__col_7extend
client_name__col_8extend
client_port_number__col_10extend
client_process_id__col_9extend
comment__col_27extend
component__col_22extend
create_time__col_38extend
database_name__col_6extend
event_timestamp__col_0extend
executed_statement__col_28extend
grantable__col_18extend
grantee_schema_name__col_32extend
hostname__col_2extend
instance_number__col_4extend
new_value__col_26extend
old_value__col_25extend
origin_database_name__col_33extend
origin_user_name__col_34extend
parameter__col_24extend
policy_name__col_11extend
port_number__col_5extend
privilege_name__col_17extend
role_name__col_19extend
role_schema_name__col_31extend
section__col_23extend
service_name__col_1extend
session_id__col_29extend
session_user__col_14extend
sid__col_3extend
statement_user_name__col_37extend
target_object__col_16extend
target_principal__col_20extend
target_schema__col_15extend
xs_application_user_name__col_35extend
xsa_attachment_id__col_43extend
xsa_attachment_name__col_44extend
xsa_binding_id__col_48extend
xsa_channel__col_42extend
xsa_data_subject__col_50extend
xsa_instance_id__col_47extend
xsa_message_ip__col_39extend
xsa_object__col_49extend
xsa_organization_id__col_45extend
xsa_space_id__col_46extend
xsa_tenant__col_40extend
xsa_uuid__col_41extend
AlertRuleUniqueNameextend

Successful logon from IP and failure from a different IP

#
Status
available
Severity
medium
Time window
1d
Group by
AccountUPN, FailedIPAddress, Name, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

Identifies when a user account successfully logs onto an Azure App from one IP and within 10 mins failed to logon to the same App via a different IP (may indicate a malicious attempt at password guessing with known account). UEBA added for context to gather all asoociated information assocaited with IP addressed initiating Faile Logon and affected user. Please note, Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges after careful consideration

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Telemetry Coverage

Rule Body

id: 02ef8d7e-fc3a-4d86-a457-650fa571d8d2
name: Successful logon from IP and failure from a different IP
description: |
  'Identifies when a user account successfully logs onto an Azure App from one IP and within 10 mins failed to logon to the same App via a different IP (may indicate a malicious attempt at password guessing with known account). 
  UEBA added for context to gather all asoociated information assocaited with IP addressed initiating Faile Logon and affected user. 
  Please note, Failed logons from known IP ranges can be benign depending on the conditional access policies. In case of noisy behavior, consider tuning the source IP ranges after careful consideration'
severity: Medium 
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - BehaviorAnalytics
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1110
  - T1078
query: |
  let riskScoreCutoff = 3; //Adjust this score threshold based on volume of results. Activities identified as the most abnormal receive the highest scores (on a scale of 0-10)
  let logonDiff = 10m; 
  let aadFunc = (tableName:string)
  { 
  table(tableName)
  | where ResultType == "0"
  | where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online") // To remove false-positives, add more Apps to this array
  // ---------- Fix for SuccessBlock to also consider IPv6
  | extend SuccessIPv6Block = strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1], ":", split(IPAddress, ":")[2], ":", split(IPAddress, ":")[3])
  | extend SuccessIPv4Block = strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])
  // ------------------
  | project SuccessLogonTime = TimeGenerated, UserPrincipalName, SuccessIPAddress = IPAddress, SuccessLocation = Location, AppDisplayName, SuccessIPBlock = iff(IPAddress contains ":", strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1]), strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])), Type
  | join kind= inner (
      table(tableName)
      | where ResultType !in ("0", "50140")
      | where ResultDescription !~ "Other"
      | where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
      | project FailedLogonTime = TimeGenerated, UserPrincipalName, FailedIPAddress = IPAddress, FailedLocation = Location, AppDisplayName, ResultType, ResultDescription, Type 
  ) on UserPrincipalName, AppDisplayName
  | where SuccessLogonTime < FailedLogonTime and FailedLogonTime - SuccessLogonTime <= logonDiff and FailedIPAddress !startswith SuccessIPBlock
  | summarize FailedLogonTime = max(FailedLogonTime), SuccessLogonTime = max(SuccessLogonTime) by UserPrincipalName, SuccessIPAddress, SuccessLocation, AppDisplayName, FailedIPAddress, FailedLocation, ResultType, ResultDescription, Type
  | extend timestamp = SuccessLogonTime
  | extend UserPrincipalName = tolower(UserPrincipalName)};
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  union isfuzzy=true aadSignin, aadNonInt
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
  // UEBA context below - make sure you have these 2 datatypes, otherwise the query will not work. If so, comment all that is below.
  | join kind=leftouter (
      IdentityInfo
      | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
      | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
      | summarize
          Tags = make_set(Tags, 1000),
          GroupMembership = make_set(GroupMembership, 1000),
          AssignedRoles = make_set(AssignedRoles, 1000),
          UserType = make_set(UserType, 1000),
          UserAccountControl = make_set(UserType, 1000)
      by AccountUPN
      | extend UserPrincipalName=tolower(AccountUPN)
  ) on UserPrincipalName
  //Below it will be joined with BehaviorAnalytics table to the Failed IP Addresses
  | join kind=leftouter (
      BehaviorAnalytics
      | where ActivityType in ("FailedLogOn", "LogOn")
      | where isnotempty(SourceIPAddress)
      | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
      | project-rename FailedIPAddress = SourceIPAddress, Name = UserName
      | summarize
          MaxInvestigationScore = max(InvestigationPriority)  // Only retrieve maximum Investigation Property score for both FailedIP and User
      by FailedIPAddress, Name)
  on FailedIPAddress, Name  // Joining on both IP and User so as to only return context associated with same user
  | extend UEBARiskScore = MaxInvestigationScore
  | project-away *1 // removing duplicate columns post outer join from output
  | where  UEBARiskScore > riskScoreCutoff
  | sort by UEBARiskScore desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SuccessIPAddress
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: FailedIPAddress
version: 2.1.11
kind: Scheduled

Stages and Predicates

Parameters

let riskScoreCutoff = 3;
let logonDiff = 10m;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let Binding: aadFunc

let aadFunc = (tableName:string)
{ 
table(tableName)
| where ResultType == "0"
| where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
| extend SuccessIPv6Block = strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1], ":", split(IPAddress, ":")[2], ":", split(IPAddress, ":")[3])
| extend SuccessIPv4Block = strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])
| project SuccessLogonTime = TimeGenerated, UserPrincipalName, SuccessIPAddress = IPAddress, SuccessLocation = Location, AppDisplayName, SuccessIPBlock = iff(IPAddress contains ":", strcat(split(IPAddress, ":")[0], ":", split(IPAddress, ":")[1]), strcat(split(IPAddress, ".")[0], ".", split(IPAddress, ".")[1])), Type
| join kind= inner (
    table(tableName)
    | where ResultType !in ("0", "50140")
    | where ResultDescription !~ "Other"
    | where AppDisplayName !in ("Office 365 Exchange Online", "Skype for Business Online")
    | project FailedLogonTime = TimeGenerated, UserPrincipalName, FailedIPAddress = IPAddress, FailedLocation = Location, AppDisplayName, ResultType, ResultDescription, Type 
) on UserPrincipalName, AppDisplayName
| where SuccessLogonTime < FailedLogonTime and FailedLogonTime - SuccessLogonTime <= logonDiff and FailedIPAddress !startswith SuccessIPBlock
| summarize FailedLogonTime = max(FailedLogonTime), SuccessLogonTime = max(SuccessLogonTime) by UserPrincipalName, SuccessIPAddress, SuccessLocation, AppDisplayName, FailedIPAddress, FailedLocation, ResultType, ResultDescription, Type
| extend timestamp = SuccessLogonTime
| extend UserPrincipalName = tolower(UserPrincipalName)};

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]) | join kind=leftouter (
    IdentityInfo
    | summarize LatestReportTime = arg_max(TimeGenerated, *) by AccountUPN
    | project AccountUPN, Tags, JobTitle, GroupMembership, AssignedRoles, UserType, IsAccountEnabled
    | summarize
        Tags = make_set(Tags, 1000),
        GroupMembership = make_set(GroupMembership, 1000),
        AssignedRoles = make_set(AssignedRoles, 1000),
        UserType = make_set(UserType, 1000),
        UserAccountControl = make_set(UserType, 1000)
    by AccountUPN
    | extend UserPrincipalName=tolower(AccountUPN)
) on UserPrincipalName | join kind=leftouter (
    BehaviorAnalytics
    | where ActivityType in ("FailedLogOn", "LogOn")
    | where isnotempty(SourceIPAddress)
    | project UsersInsights, DevicesInsights, ActivityInsights, InvestigationPriority, SourceIPAddress, UserName
    | project-rename FailedIPAddress = SourceIPAddress, Name = UserName
    | summarize
        MaxInvestigationScore = max(InvestigationPriority)
    by FailedIPAddress, Name)
on FailedIPAddress, Name | extend UEBARiskScore = MaxInvestigationScore | project-away *1 | where  UEBARiskScore > riskScoreCutoff | sort by UEBARiskScore desc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
FailedIPAddresssummarize
Namesummarize

Suspicious application consent for offline access

#
Status
available
Severity
low
Time window
14d
Group by
AppClientId, CorrelationId
Source
github.com/Azure/Azure-Sentinel

This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth. Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication. Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome! For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 3533f74c-9207-4047-96e2-0eb9383be587
name: Suspicious application consent for offline access
description: |
  'This will alert when a user consents to provide a previously-unknown Azure application with offline access via OAuth.
  Offline access will provide the Azure App with access to the listed resources without requiring two-factor authentication.
  Consent to applications with offline access and read capabilities should be rare, especially as the knownApplications list is expanded. Public contributions to expand this filter are welcome!
  For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
relevantTechniques:
  - T1528
query: |
  let detectionTime = 1d;
  let joinLookback = 14d;
  AuditLogs
  | where TimeGenerated > ago(detectionTime)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Consent to application"
  | where TargetResources has "offline"
  | mv-apply TargetResource=TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal"
        | extend ModifiedProperties = TargetResource.modifiedProperties,
                 AppDisplayName = tostring(TargetResource.displayName),
                 AppClientId = tolower(tostring(TargetResource.id))
    )
  | where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))
  | mv-apply Properties=ModifiedProperties on 
    (
        where Properties.displayName =~ "ConsentAction.Permissions"
        | extend ConsentFull = tostring(Properties.newValue)
        | extend ConsentFull = trim(@'"',tostring(ConsentFull))
    )
  | parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *
  | where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")
  | where GrantConsentType != "AllPrincipals" // NOTE: we are ignoring if OAuth application was granted to all users via an admin - but admin due diligence should be audited occasionally
  | extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
  | extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
  | extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
  | extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
  | extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId
  | join kind = leftouter (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add service principal"
  | mv-apply TargetResource=TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal"
        | extend ModifiedProperties = TargetResource.modifiedProperties,
                 AppClientId = tolower(TargetResource.id)
    )
  | mv-apply ModifiedProperties=TargetResource.modifiedProperties on 
     (
        where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
        | extend AppReplyURLs = ModifiedProperties.newValue
     )
   | distinct AppClientId, tostring(AppReplyURLs)
  )
  on AppClientId
  | join kind = innerunique (AuditLogs
  | where TimeGenerated > ago(joinLookback)
  | where LoggedByService =~ "Core Directory"
  | where Category =~ "ApplicationManagement"
  | where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
   | mv-apply TargetResource=TargetResources on 
    (
        where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
        | extend GrantAuthentication = tostring(TargetResource.displayName)
    )
  | extend GrantOperation = OperationName
  | project GrantAuthentication, GrantOperation, CorrelationId
  ) on CorrelationId
  | project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, AppReplyURLs, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, GrantAuthentication, OperationName, GrantOperation, CorrelationId, ConsentFull
  | extend Name = tostring(split(GrantInitiatedByUserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(GrantInitiatedByUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: GrantInitiatedByUserPrincipalName
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: GrantInitiatedByAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: GrantIpAddress
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let detectionTime = 1d;
let joinLookback = 14d;

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated > ago(detectionTime)

Stage 3: where

| where LoggedByService =~ "Core Directory"

Stage 4: where

| where Category =~ "ApplicationManagement"

Stage 5: where

| where OperationName =~ "Consent to application"

Stage 6: where

| where TargetResources has "offline"

Stage 7: kusto:mv-apply

| mv-apply TargetResource=TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal"
      | extend ModifiedProperties = TargetResource.modifiedProperties,
               AppDisplayName = tostring(TargetResource.displayName),
               AppClientId = tolower(tostring(TargetResource.id))
  )

Stage 8: where

| where AppClientId !in ((externaldata(knownAppClientId:string, knownAppDisplayName:string)[@"https://raw.githubusercontent.com/Azure/Azure-Sentinel/master/Sample%20Data/Feeds/Microsoft.OAuth.KnownApplications.csv"] with (format="csv")))

Stage 9: kusto:mv-apply

| mv-apply Properties=ModifiedProperties on 
  (
      where Properties.displayName =~ "ConsentAction.Permissions"
      | extend ConsentFull = tostring(Properties.newValue)
      | extend ConsentFull = trim(@'"',tostring(ConsentFull))
  )

Stage 10: parse

| parse ConsentFull with * "ConsentType: " GrantConsentType ", Scope: " GrantScope1 "]" *

Stage 11: where

| where ConsentFull has "offline_access" and ConsentFull has_any ("Files.Read", "Mail.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read", "Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All")

Stage 12: where

| where GrantConsentType != "AllPrincipals"

Stage 13: extend (7 consecutive steps)

| extend GrantInitiatedByAppName = tostring(InitiatedBy.app.displayName)
| extend GrantInitiatedByAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend GrantInitiatedByUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend GrantInitiatedByAadUserId = tostring(InitiatedBy.user.id)
| extend GrantIpAddress = iff(isnotempty(InitiatedBy.user.ipAddress), tostring(InitiatedBy.user.ipAddress), tostring(InitiatedBy.app.ipAddress))
| extend GrantInitiatedBy = iff(isnotempty(GrantInitiatedByUserPrincipalName), GrantInitiatedByUserPrincipalName, GrantInitiatedByAppName)
| extend GrantUserAgent = tostring(iff(AdditionalDetails[0].key =~ "User-Agent", AdditionalDetails[0].value, ""))

Stage 14: project

| project TimeGenerated, GrantConsentType, GrantScope1, GrantInitiatedBy, AppDisplayName, GrantInitiatedByUserPrincipalName, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantIpAddress, GrantUserAgent, AppClientId, OperationName, ConsentFull, CorrelationId

Stage 15: join

| join kind = leftouter (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add service principal"
| mv-apply TargetResource=TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal"
      | extend ModifiedProperties = TargetResource.modifiedProperties,
               AppClientId = tolower(TargetResource.id)
  )
| mv-apply ModifiedProperties=TargetResource.modifiedProperties on 
   (
      where ModifiedProperties.displayName =~ "AppAddress" and ModifiedProperties.newValue has "AddressType"
      | extend AppReplyURLs = ModifiedProperties.newValue
   )
 | distinct AppClientId, tostring(AppReplyURLs)
)
on AppClientId

Stage 16: join

| join kind = innerunique (AuditLogs
| where TimeGenerated > ago(joinLookback)
| where LoggedByService =~ "Core Directory"
| where Category =~ "ApplicationManagement"
| where OperationName =~ "Add OAuth2PermissionGrant" or OperationName =~ "Add delegated permission grant"
 | mv-apply TargetResource=TargetResources on 
  (
      where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
      | extend GrantAuthentication = tostring(TargetResource.displayName)
  )
| extend GrantOperation = OperationName
| project GrantAuthentication, GrantOperation, CorrelationId
) on CorrelationId

Stage 17: project

project AppClientId, AppDisplayName, AppReplyURLs, ConsentFull, CorrelationId, GrantAuthentication, GrantConsentType, GrantInitiatedBy, GrantInitiatedByAadUserId, GrantInitiatedByAppName, GrantInitiatedByAppServicePrincipalId, GrantInitiatedByUserPrincipalName, GrantIpAddress, GrantOperation, GrantScope1, GrantUserAgent, OperationName, TimeGenerated

Stage 18: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • ApplicationManagement corpus 13 (kusto 13)
field:"Category" kind:eq value:"ApplicationManagement"
ConsentFullmatch
  • ChannelMessage.Read transforms: term
  • Chat.Read transforms: term
  • EAS.AccessAsUser.All transforms: term
  • EWS.AccessAsUser.All transforms: term
  • Files.Read transforms: term
  • Group.Read transforms: term
  • Mail.Read transforms: term
  • Notes.Read transforms: term
  • TeamsActivity.Read transforms: term
  • offline_access transforms: term
field:"ConsentFull" kind:match
GrantConsentTypene
  • AllPrincipals
field:"GrantConsentType" kind:ne value:"AllPrincipals"
LoggedByServiceeq
  • Core Directory corpus 11 (kusto 10, sigma 1)
  • Core Directory
field:"azure_ad::logged_by_service" kind:eq
ModifiedProperties.displayNameeq
  • AppAddress transforms: kusto_dynamic_path
field:"ModifiedProperties.displayName" kind:eq value:"AppAddress"
ModifiedProperties.newValuematch
  • AddressType transforms: term, kusto_dynamic_path
field:"ModifiedProperties.newValue" kind:match value:"AddressType"
OperationNameeq
  • Add OAuth2PermissionGrant
  • Add delegated permission grant
  • Add service principal
  • Consent to application corpus 5 (kusto 5)
field:"OperationName" kind:eq
Properties.displayNameeq
  • ConsentAction.Permissions transforms: kusto_dynamic_path
field:"Properties.displayName" kind:eq value:"ConsentAction.Permissions"
TargetResource.displayNameis_not_null
  • (no value, null check)
field:"TargetResource.displayName" kind:is_not_null
TargetResource.typeeq
  • ServicePrincipal transforms: kusto_dynamic_path
field:"TargetResource.type" kind:eq value:"ServicePrincipal"
TargetResourcesmatch
  • offline transforms: term corpus 2 (kusto 2)
field:"azure_ad::target_resources" kind:match value:"offline"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AppClientIdproject
AppDisplayNameproject
AppReplyURLsproject
ConsentFullproject
CorrelationIdproject
GrantAuthenticationproject
GrantConsentTypeproject
GrantInitiatedByproject
GrantInitiatedByAadUserIdproject
GrantInitiatedByAppNameproject
GrantInitiatedByAppServicePrincipalIdproject
GrantInitiatedByUserPrincipalNameproject
GrantIpAddressproject
GrantOperationproject
GrantScope1project
GrantUserAgentproject
OperationNameproject
TimeGeneratedproject
Nameextend
UPNSuffixextend

TIE Indicators of Attack

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

'Searches for triggered Indicators of Attack.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 6c75f0d2-2973-4188-bb05-ec7bc8696120
name: TIE Indicators of Attack
description: |
  'Searches for triggered Indicators of Attack.'
severity: Low
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
    "low", 1,
    "medium", 2,
    "high", 3,
    "critical", 4
    ];
    afad_parser
    | where MessageType == 2
    | lookup kind=leftouter SeverityTable on Severity
    | order by Level
    | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

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

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 2

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
MessageTypeeq
  • 2 corpus 21 (kusto 21)
field:"MessageType" kind:eq value:"2"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

Time series anomaly detection for total volume of traffic

#
Severity
medium
Time window
14d
Group by
AnomalyHour, DeviceVendor, Total, anomalies, baseline, score
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

'Identifies anamalous spikes in network traffic logs as compared to baseline or normal historical patterns. The query leverages a KQL built-in anomaly detection algorithm to find large deviations from baseline patterns. Sudden increases in network traffic volume may be an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port traffic observed in the flagged anomaly hour. The source IP addresses which were sending less than percentotalthreshold of the total traffic have been exluded whose value can be adjusted as needed . You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: 06a9b845-6a95-4432-a78b-83919b28c375
name: Time series anomaly detection for total volume of traffic
description: |
  'Identifies anamalous spikes in network traffic logs as compared to baseline or normal historical patterns.
  The query leverages a KQL built-in anomaly detection algorithm to find large deviations from baseline patterns.
  Sudden increases in network traffic volume may be an indication of data exfiltration attempts and should be investigated.
  The higher the score, the further it is from the baseline value.
  The output is aggregated to provide summary view of unique source IP to destination IP address and port traffic observed in the flagged anomaly hour.
  The source IP addresses which were sending less than percentotalthreshold of the total traffic have been exluded whose value can be adjusted as needed .
  You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
severity: Medium
requiredDataConnectors:
  - connectorId: Barracuda
    dataTypes:
      - CommonSecurityLog
  - connectorId: CEF
    dataTypes:
      - CommonSecurityLog
  - connectorId: CheckPoint
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: F5
    dataTypes:
      - CommonSecurityLog
  - connectorId: Fortinet
    dataTypes:
      - CommonSecurityLog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 3
tactics:
  - Exfiltration
relevantTechniques:
  - T1030
query: |
  let starttime = 14d;
  let endtime = 1d;
  let timeframe = 1h;
  let scorethreshold = 5;
  let percentotalthreshold = 50;
  let TimeSeriesData = CommonSecurityLog
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | project TimeGenerated,SourceIP, DestinationIP, DeviceVendor
  | make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor;
  // Filtering specific records associated with spikes as outliers
  let TimeSeriesAlerts=materialize(TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, scorethreshold, -1, 'linefit')
  | mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
  | where anomalies > 0 | extend score = round(score,2), AnomalyHour = TimeGenerated
  | project DeviceVendor,AnomalyHour, TimeGenerated, Total, baseline, anomalies, score);
  let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
  // Join anomalies with Base Data to popalate associated records for investigation - Results sorted by score in descending order
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join (
      CommonSecurityLog
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | summarize HourlyCount = count(), TimeGeneratedMax = arg_max(TimeGenerated, *), DestinationIPlist = make_set(DestinationIP, 100), DestinationPortlist = make_set(DestinationPort, 100) by DeviceVendor, SourceIP, TimeGeneratedHour= bin(TimeGenerated, 1h)
  | extend AnomalyHour = TimeGeneratedHour
  ) on AnomalyHour, DeviceVendor
  | extend PercentTotal = round((HourlyCount / Total) * 100, 3)
  | where PercentTotal > percentotalthreshold
  | project DeviceVendor , AnomalyHour, TimeGeneratedMax, SourceIP, DestinationIPlist, DestinationPortlist, HourlyCount, PercentTotal, Total, baseline, score, anomalies
  | summarize HourlyCount=sum(HourlyCount), StartTimeUtc=min(TimeGeneratedMax), EndTimeUtc=max(TimeGeneratedMax), SourceIPlist = make_set(SourceIP, 100), SourceIPMax= arg_max(SourceIP, *), DestinationIPlist = make_set(DestinationIPlist, 100), DestinationPortlist = make_set(DestinationPortlist, 100) by DeviceVendor , AnomalyHour, Total, baseline, score, anomalies
  | project DeviceVendor , AnomalyHour, EndTimeUtc, SourceIPMax ,SourceIPlist, DestinationIPlist, DestinationPortlist, HourlyCount, Total, baseline, score, anomalies

entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIPMax
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let percentotalthreshold = 50;

let TimeSeriesData and let TimeSeriesAlerts are inlined into the numbered stages below.

Let Binding: AnomalyHours used in Stage 12

let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);

Stages 1 to 10 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 11 to 17 use it.

Stage 1: source

CommonSecurityLog

Stage 2: where

| where isnotempty(DestinationIP) and isnotempty(SourceIP)

Stage 3: where

| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))

Stage 4: project

| project TimeGenerated,SourceIP, DestinationIP, DeviceVendor

The stages below score time-series anomalies (make-series, series_decompose_anomalies).

Stage 5: make-series

| make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor

Stage 6: extend

| extend (anomalies, score, baseline) = series_decompose_anomalies(Total, scorethreshold, -1, 'linefit')

Stage 7: mv-expand

| mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)

Stage 8: where

| where anomalies > 0

Stage 9: extend

| extend score = round(score,2), AnomalyHour = TimeGenerated

Stage 10: project

| project DeviceVendor,AnomalyHour, TimeGenerated, Total, baseline, anomalies, score

Stage 11: where

TimeSeriesAlerts
| where TimeGenerated > ago(2d)

Stage 12: join

| join (
    CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| summarize HourlyCount = count(), TimeGeneratedMax = arg_max(TimeGenerated, *), DestinationIPlist = make_set(DestinationIP, 100), DestinationPortlist = make_set(DestinationPort, 100) by DeviceVendor, SourceIP, TimeGeneratedHour= bin(TimeGenerated, 1h)
| extend AnomalyHour = TimeGeneratedHour
) on AnomalyHour, DeviceVendor

Stage 13: extend

extend PercentTotal

Stage 14: where

where PercentTotal > 50

Stage 15: project

project AnomalyHour, DestinationIPlist, DestinationPortlist, DeviceVendor, HourlyCount, PercentTotal, SourceIP, TimeGeneratedMax, Total, anomalies, baseline, score

Stage 16: summarize

summarize DestinationIPlist, DestinationPortlist, EndTimeUtc, HourlyCount, SourceIPMax, SourceIPlist, StartTimeUtc by DeviceVendor, AnomalyHour, Total, baseline, score, anomalies

Stage 17: project

project AnomalyHour, DestinationIPlist, DestinationPortlist, DeviceVendor, EndTimeUtc, HourlyCount, SourceIPMax, SourceIPlist, Total, anomalies, baseline, score

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DestinationIPis_not_null
  • (no value, null check)
field:"DestinationIP" kind:is_not_null
PercentTotalgt
  • 50 transforms: number
field:"PercentTotal" kind:gt value:"50"
SourceIPis_not_null
  • (no value, null check)
field:"SourceIP" kind:is_not_null
anomaliesgt
  • 0 transforms: number
field:"anomalies" kind:gt value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AnomalyHourproject
DestinationIPlistproject
DestinationPortlistproject
DeviceVendorproject
EndTimeUtcproject
HourlyCountproject
SourceIPMaxproject
SourceIPlistproject
Totalproject
anomaliesproject
baselineproject
scoreproject

Zoom E2E Encryption Disabled

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

'This alerts when end to end encryption is disabled for Zoom meetings.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access
Discovery

Rule Body

id: e4779bdc-397a-4b71-be28-59e6a1e1d16b
name: Zoom E2E Encryption Disabled
description: |
  'This alerts when end to end encryption is disabled for Zoom meetings.'
severity: Medium
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - Discovery
relevantTechniques:
  - T1040
query: |
  ZoomLogs
  | where Event =~ "account.settings_updated"
  | extend NewE2ESetting = columnifexists("payload_object_settings_in_meeting_e2e_encryption_b", "")
  | extend OldE2ESetting = columnifexists("payload_old_object_settings_in_meeting_e2e_encryption_b", "")
  | where OldE2ESetting =~ 'false' and NewE2ESetting =~ 'true'
  | extend AccountName = tostring(split(User, "@")[0]), AccountUPNSuffix = tostring(split(User, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
version: 1.0.3
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Stage 1: source

ZoomLogs

Stage 2: where

| where Event =~ "account.settings_updated"

Stage 3: extend

| extend NewE2ESetting = columnifexists("payload_object_settings_in_meeting_e2e_encryption_b", "")

Stage 4: extend

| extend OldE2ESetting = columnifexists("payload_old_object_settings_in_meeting_e2e_encryption_b", "")

Stage 5: where

| where OldE2ESetting =~ 'false' and NewE2ESetting =~ 'true'

Stage 6: extend

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
NewE2ESettingextend
OldE2ESettingextend
AccountNameextend
AccountUPNSuffixextend

Back to all Kusto rules