Detection rules › Kusto

Kusto rules: 28

1Password - Changes to firewall rules

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

This will alert when changes have been made to the firewall rules. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same. Ref: https://1password.com/ Ref: https://github.com/securehats/

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Telemetry Coverage

Rule Body

id: 54e6bb8e-2935-422f-9387-dba1961abfd7
name: 1Password - Changes to firewall rules
version: 1.0.0
kind: Scheduled
description: |-
  This will alert when changes have been made to the firewall rules. Once this analytics rule is triggered it will group all related future alerts for upto an hour when all related entities are the same.

  Ref: https://1password.com/
  Ref: https://github.com/securehats/
severity: Medium
requiredDataConnectors:
  - connectorId: 1Password
    dataTypes:
      - OnePasswordEventLogs_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |-
  OnePasswordEventLogs_CL
  | where log_source == "auditevents"
  | where action == "updatfw"
  | where object_type == "account"
  | extend
      ActorUsername = actor_details.email
      , SrcIpAddr = session.ip
subTechniques:
- T1562.007
suppressionEnabled: false
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 1h
    matchingMethod: AllEntities
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: FullName
    columnName: ActorUsername
- entityType: IP
  fieldMappings:
  - identifier: Address
    columnName: SrcIpAddr

Stages and Predicates

Stage 1: source

OnePasswordEventLogs_CL

Stage 2: where

| where log_source == "auditevents"

Stage 3: where

| where action == "updatfw"

Stage 4: where

| where object_type == "account"

Stage 5: extend

| extend
    ActorUsername = actor_details.email
    , SrcIpAddr = session.ip

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorUsernameextend
SrcIpAddrextend

Anomaly found in Network Session Traffic (ASIM Network Session schema)

#
Status
available
Severity
medium
Time window
14d
Group by
NetworkCustomAnalyticsExists, NetworkSummary_Protocol_Exists, exists
Source
github.com/Azure/Azure-Sentinel

The rule identifies anomalous pattern in network session traffic based on previously seen data, different Device Action, Network Protocol, Network Direction or overall volume. The rule utilize ASIM normalization, and is applied to any source which supports the ASIM Network Session schema. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: cd6def0d-3ef0-4d55-a7e3-faa96c46ba12
name: Anomaly found in Network Session Traffic (ASIM Network Session schema)
description: |
  'The rule identifies anomalous pattern in network session traffic based on previously seen data, different Device Action, Network Protocol, Network Direction or overall volume. The rule utilize [ASIM](https://aka.ms/AboutASIM) normalization, and is applied to any source which supports the ASIM Network Session schema. This rule leverages log summaries generated by a Summary Rule or Summarized Playbook. If no such summaries are available, the rule falls back to direct analysis using ASIM function.'
severity: Medium
status: Available 
tags:
  - 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: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
  - Discovery
  - Exfiltration
  - LateralMovement
relevantTechniques:
  - T1095
  - T1071
  - T1046
  - T1030
  - T1210
query: |
  let min_t = ago(14d);
  let max_t = now();
  let dt = 1d;
  let fieldForDvcAction = "DvcAction";
  let fieldForNetworkDirection = "NetworkDirection";
  let fieldForNetworkProtocol = "NetworkProtocol";
  let AnomalyThreshold = 2.5;
  let eps = materialize (_Im_NetworkSession
      | project TimeGenerated
      | where TimeGenerated > ago(5m)
      | count
      | extend Count = Count / 300);
  let maxSummarizedTime = toscalar (
      union isfuzzy=true
          (
          NetworkSummary_Protocol_CL
          | where EventTime > min_t
          | summarize max_TimeGenerated=max(EventTime)
          | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
          ),
          (
          NetworkCustomAnalytics_protocol_CL
          | where EventTime_t > min_t
          | summarize max_TimeGenerated=max(EventTime_t)
          | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
          ),
          (
          print(min_t)
          | project max_TimeGenerated = print_0
          )
      | summarize maxTimeGenerated = max(max_TimeGenerated) 
      );
  let NetworkSummary_Protocol = materialize(
      union isfuzzy=true 
          (
          NetworkSummary_Protocol_CL
          | where EventTime > ago(1d)
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkSummary_Protocol_Exists = (maxv > 1),
          NetworkCustomAnalyticsExists = false
      );
  let NetworkCustomAnalytics = materialize(
      union isfuzzy=true 
          (
          NetworkCustomAnalytics_protocol_CL
          | where EventTime_t > ago(1d) 
          | project v = int(2)
          ),
          (
          print int(1) 
          | project v = print_0
          )
      | summarize maxv = max(v)
      | extend
          NetworkCustomAnalyticsExists = (maxv > 1),
          NetworkSummary_Protocol_Exists = false
      );
  let allData = union isfuzzy=true 
          (
          (datatable(
      exists: int,
      NetworkSummary_Protocol_Exists: bool,
      NetworkCustomAnalyticsExists: bool
  )[
      1, false, false
  ]
          | where toscalar(eps) > 500
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists)
          | join (
              _Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
              | where TimeGenerated > maxSummarizedTime
              | summarize Count=count()
                  by
                  NetworkProtocol,
                  DstPortNumber,
                  DstAppName,
                  NetworkDirection,
                  DvcAction,
                  bin(TimeGenerated, 10m)
              | extend
                  EventTime = TimeGenerated,
                  Count = toint(Count),
                  DstPortNumber = toint(DstPortNumber),
                  exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          (datatable(
      exists: int,
      NetworkSummary_Protocol_Exists: bool,
      NetworkCustomAnalyticsExists: bool
  )[
      1, false, false
  ]
          | where toscalar(eps) <= 500
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
          )
          | join (
              _Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
              | where TimeGenerated > maxSummarizedTime
              | summarize Count=count()
                  by
                  NetworkProtocol,
                  DstPortNumber,
                  DstAppName,
                  NetworkDirection,
                  DvcAction,
                  bin(TimeGenerated, 10m)
              | extend
                  EventTime = TimeGenerated,
                  Count = toint(Count),
                  DstPortNumber = toint(DstPortNumber),
                  exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          (datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
          | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
          )
          | join (
              NetworkCustomAnalytics_protocol_CL
              | where EventTime_t > min_t
              | project
                  NetworkProtocol=NetworkProtocol_s,
                  DstPortNumber=DstPortNumber_d,
                  DstAppName=DstAppName_s,
                  NetworkDirection=NetworkDirection_s,
                  DvcAction=DvcAction_s,
                  Count=count__d,
                  EventTime=EventTime_t,
                  TimeGenerated,
                  Type
              | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
              )
              on exists
          | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
          ),
          (
          NetworkSummary_Protocol_CL
          | where EventTime > min_t
          | project
              NetworkProtocol,
              DstPortNumber,
              DstAppName,
              NetworkDirection,
              DvcAction,
              Count=count_,
              EventTime,
              TimeGenerated,
              Type
          | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
          )
  ;
  let findVolumneBasedAnomaly = allData
      | make-series total=sum(Count) on EventTime from min_t to max_t step dt
      | extend (anomalies, score, baseline) = series_decompose_anomalies(total, AnomalyThreshold, -1, 'linefit') 
      | mv-expand anomalies, score, baseline, EventTime, total
      | extend
          anomalies = toint(anomalies),
          score = toint(score),
          baseline = toint(baseline),
          EventTime = todatetime(EventTime),
          total = tolong(total)
      | where EventTime >= ago(1d)
      | where score >= 2 * AnomalyThreshold
  ;
  let findAnomalies = (field: string) {
      allData
      | where isnotempty(column_ifexists(field, ""))
      | make-series total=sum(Count) on EventTime from min_t to max_t step dt by column_ifexists(field, "")
      | extend (anomalies, score, baseline) = series_decompose_anomalies(total, AnomalyThreshold, -1, 'linefit')
      | mv-expand anomalies, score, baseline, EventTime, total
      | extend
          anomalies = toint(anomalies),
          score = toint(score),
          baseline = toint(baseline),
          EventTime = todatetime(EventTime),
          total = tolong(total)
      | where EventTime >= ago(1d)
      | where score >= 2 * AnomalyThreshold
  };
  union
      findAnomalies(fieldForDvcAction),
      findAnomalies(fieldForNetworkDirection),
      findAnomalies(fieldForNetworkProtocol),
      findVolumneBasedAnomaly
  | extend
      anomalyFieldType  = case (
                          isnotempty(column_ifexists(fieldForDvcAction, "")),
                          "DvcAction",
                          isnotempty(column_ifexists(fieldForNetworkDirection, "")),
                          "NetworkDirection",
                          isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
                          "NetworkProtocol",
                          "TotalVolume"
                      ),
      anomalyFieldValue  = case (
                          isnotempty(column_ifexists(fieldForDvcAction, "")),
                          column_ifexists(fieldForDvcAction, ""),
                          isnotempty(column_ifexists(fieldForNetworkDirection, "")),
                          column_ifexists(fieldForNetworkDirection, ""),
                          isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
                          column_ifexists(fieldForNetworkProtocol, ""),
                          "Overall"
                      )
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  AnomalyFieldType: anomalyFieldType
  AnomalyFieldValue: anomalyFieldValue
  Score: score

alertDetailsOverride:
  alertDisplayNameFormat: Anomaly was observed with {{anomalyFieldValue}} Traffic
  alertDescriptionFormat: 'Based on past data, anomaly was observed in {{anomalyFieldValue}} Traffic with a score of {{score}}.'
version: 1.3.0
kind: Scheduled

Stages and Predicates

Parameters

let min_t = ago(14d);
let max_t = now();
let dt = 1d;
let fieldForDvcAction = "DvcAction";
let fieldForNetworkDirection = "NetworkDirection";
let fieldForNetworkProtocol = "NetworkProtocol";
let AnomalyThreshold = 2.5;

Let Binding: eps

let eps = materialize (_Im_NetworkSession
    | project TimeGenerated
    | where TimeGenerated > ago(5m)
    | count
    | extend Count = Count / 300);

Let Binding: maxSummarizedTime

let maxSummarizedTime = toscalar (
    union isfuzzy=true
        (
        NetworkSummary_Protocol_CL
        | where EventTime > min_t
        | summarize max_TimeGenerated=max(EventTime)
        | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
        ),
        (
        NetworkCustomAnalytics_protocol_CL
        | where EventTime_t > min_t
        | summarize max_TimeGenerated=max(EventTime_t)
        | extend max_TimeGenerated = datetime_add('minute', 20, max_TimeGenerated)
        ),
        (
        print(min_t)
        | project max_TimeGenerated = print_0
        )
    | summarize maxTimeGenerated = max(max_TimeGenerated) 
    );

Let Binding: NetworkSummary_Protocol

let NetworkSummary_Protocol = materialize(
    union isfuzzy=true 
        (
        NetworkSummary_Protocol_CL
        | where EventTime > ago(1d)
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkSummary_Protocol_Exists = (maxv > 1),
        NetworkCustomAnalyticsExists = false
    );

Let Binding: NetworkCustomAnalytics

let NetworkCustomAnalytics = materialize(
    union isfuzzy=true 
        (
        NetworkCustomAnalytics_protocol_CL
        | where EventTime_t > ago(1d) 
        | project v = int(2)
        ),
        (
        print int(1) 
        | project v = print_0
        )
    | summarize maxv = max(v)
    | extend
        NetworkCustomAnalyticsExists = (maxv > 1),
        NetworkSummary_Protocol_Exists = false
    );

Let Binding: allData

let allData = union isfuzzy=true 
        (
        (datatable(
    exists: int,
    NetworkSummary_Protocol_Exists: bool,
    NetworkCustomAnalyticsExists: bool
)[
    1, false, false
]
        | where toscalar(eps) > 500
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists)
        | join (
            _Im_NetworkSession(starttime=todatetime(ago(3d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count()
                by
                NetworkProtocol,
                DstPortNumber,
                DstAppName,
                NetworkDirection,
                DvcAction,
                bin(TimeGenerated, 10m)
            | extend
                EventTime = TimeGenerated,
                Count = toint(Count),
                DstPortNumber = toint(DstPortNumber),
                exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
        ),
        (
        (datatable(
    exists: int,
    NetworkSummary_Protocol_Exists: bool,
    NetworkCustomAnalyticsExists: bool
)[
    1, false, false
]
        | where toscalar(eps) <= 500
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        | join (NetworkCustomAnalytics) on NetworkCustomAnalyticsExists
        )
        | join (
            _Im_NetworkSession(starttime=todatetime(ago(4d)), endtime=now())
            | where TimeGenerated > maxSummarizedTime
            | summarize Count=count()
                by
                NetworkProtocol,
                DstPortNumber,
                DstAppName,
                NetworkDirection,
                DvcAction,
                bin(TimeGenerated, 10m)
            | extend
                EventTime = TimeGenerated,
                Count = toint(Count),
                DstPortNumber = toint(DstPortNumber),
                exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
        ),
        (
        (datatable(exists: int, NetworkSummary_Protocol_Exists: bool)[1, false]
        | join (NetworkSummary_Protocol) on NetworkSummary_Protocol_Exists
        )
        | join (
            NetworkCustomAnalytics_protocol_CL
            | where EventTime_t > min_t
            | project
                NetworkProtocol=NetworkProtocol_s,
                DstPortNumber=DstPortNumber_d,
                DstAppName=DstAppName_s,
                NetworkDirection=NetworkDirection_s,
                DvcAction=DvcAction_s,
                Count=count__d,
                EventTime=EventTime_t,
                TimeGenerated,
                Type
            | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber), exists=int(1)
            )
            on exists
        | project-away exists*, maxv, NetworkSummary_Protocol_Exists*, NetworkCustomAnalyticsExists*
        ),
        (
        NetworkSummary_Protocol_CL
        | where EventTime > min_t
        | project
            NetworkProtocol,
            DstPortNumber,
            DstAppName,
            NetworkDirection,
            DvcAction,
            Count=count_,
            EventTime,
            TimeGenerated,
            Type
        | extend Count = toint(Count), DstPortNumber = toint(DstPortNumber)
        );

Let Binding: findVolumneBasedAnomaly

let findVolumneBasedAnomaly = allData
    | make-series total=sum(Count) on EventTime from min_t to max_t step dt
    | extend (anomalies, score, baseline) = series_decompose_anomalies(total, AnomalyThreshold, -1, 'linefit') 
    | mv-expand anomalies, score, baseline, EventTime, total
    | extend
        anomalies = toint(anomalies),
        score = toint(score),
        baseline = toint(baseline),
        EventTime = todatetime(EventTime),
        total = tolong(total)
    | where EventTime >= ago(1d)
    | where score >= 2 * AnomalyThreshold;

Let Binding: findAnomalies

let findAnomalies = (field: string) {
    allData
    | where isnotempty(column_ifexists(field, ""))
    | make-series total=sum(Count) on EventTime from min_t to max_t step dt by column_ifexists(field, "")
    | extend (anomalies, score, baseline) = series_decompose_anomalies(total, AnomalyThreshold, -1, 'linefit')
    | mv-expand anomalies, score, baseline, EventTime, total
    | extend
        anomalies = toint(anomalies),
        score = toint(score),
        baseline = toint(baseline),
        EventTime = todatetime(EventTime),
        total = tolong(total)
    | where EventTime >= ago(1d)
    | where score >= 2 * AnomalyThreshold
};

Union (4 Sources)

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

Leg 1: findAnomalies

findAnomalies(fieldForDvcAction)

Leg 2: findAnomalies

findAnomalies(fieldForNetworkDirection)

Leg 3: findAnomalies

findAnomalies(fieldForNetworkProtocol)

Leg 4: findVolumneBasedAnomaly

Applied to the Combined Result

| extend
    anomalyFieldType  = case (
                        isnotempty(column_ifexists(fieldForDvcAction, "")),
                        "DvcAction",
                        isnotempty(column_ifexists(fieldForNetworkDirection, "")),
                        "NetworkDirection",
                        isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
                        "NetworkProtocol",
                        "TotalVolume"
                    ),
    anomalyFieldValue  = case (
                        isnotempty(column_ifexists(fieldForDvcAction, "")),
                        column_ifexists(fieldForDvcAction, ""),
                        isnotempty(column_ifexists(fieldForNetworkDirection, "")),
                        column_ifexists(fieldForNetworkDirection, ""),
                        isnotempty(column_ifexists(fieldForNetworkProtocol, "")),
                        column_ifexists(fieldForNetworkProtocol, ""),
                        "Overall"
                    )

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
scorege
  • 5
field:"score" kind:ge value:"5"

Output Fields

These fields are emitted when the rule matches.

FieldSource
totalextend
anomaliesextend
baselineextend
scoreextend
EventTimeextend
anomalyFieldTypeextend
anomalyFieldValueextend

App GW WAF - Code Injection

#
Status
available
Severity
high
Time window
6h
Group by
Action, ClientIp, TransactionId, Uri
Source
github.com/Azure/Azure-Sentinel

'Identifies a match for a Code Injection based attack in the App Gateway WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements. References: https://owasp.org/www-community/attacks/Code_Injection'

MITRE ATT&CK Coverage

Rule Body

id: 912a18fc-6165-446b-8740-81ae6c3f75ee
name: App GW WAF - Code Injection 
description: | 
  'Identifies a match for a Code Injection based attack in the App Gateway WAF logs. The threshold value in the query can be changed as per your infrastructure's requirements.
  References: https://owasp.org/www-community/attacks/Code_Injection'
severity: High 
status: Available 
requiredDataConnectors: 
  - connectorId: WAF
    dataTypes: 
      - AzureDiagnostics
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - DefenseEvasion
  - Execution
  - InitialAccess
  - PrivilegeEscalation
relevantTechniques:
  - T1548
  - T1203
  - T1190
  - T1548
tags:
  - Code Injection
query:  | 
 let Threshold = 3;  
 AGWFirewallLogs
 | where Action == "Matched"
 | where Message has "Injection" or Message has "File Inclusion"
 | project TransactionId, Hostname, RequestUri, TimeGenerated, ClientIp, Message, DetailedMessage, DetailedData
 | join kind = inner(
 AGWFirewallLogs
 | where Action == "Blocked") on TransactionId
 | extend Uri = strcat(Hostname,RequestUri)
 | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), TransactionID = make_set(TransactionId,100), Message = make_set(Message,100), Detail_Message = make_set(DetailedMessage,100), Detail_Data = make_set(DetailedData,100), Total_TransactionId = dcount(TransactionId) by ClientIp, Uri, Action
 | where Total_TransactionId >= Threshold
# The Threshold value above can be changed as per your infrastructure's requirement
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Uri
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIp 
version: 1.0.3
kind: Scheduled

Stages and Predicates

Parameters

let Threshold = 3;

Stage 1: source

AGWFirewallLogs

Stage 2: where

| where Action == "Matched"

Stage 3: where

| where Message has "Injection" or Message has "File Inclusion"

Stage 4: project

| project TransactionId, Hostname, RequestUri, TimeGenerated, ClientIp, Message, DetailedMessage, DetailedData

Stage 5: join

| join kind = inner(
AGWFirewallLogs
| where Action == "Blocked") on TransactionId

Stage 6: extend

extend Uri

Stage 7: summarize

summarize Detail_Data, Detail_Message, EndTime, Message, StartTime, Total_TransactionId, TransactionID by ClientIp, Uri, Action

Stage 8: where

where Total_TransactionId >= 3

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actioneq
  • Blocked
  • Matched
field:"Action" kind:eq
Messagematch
  • File Inclusion transforms: term
  • Injection transforms: term
field:"Message" kind:match
Total_TransactionIdge
  • 3 transforms: number
field:"Total_TransactionId" kind:ge value:"3"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionsummarize
ClientIpsummarize
Detail_Datasummarize
Detail_Messagesummarize
EndTimesummarize
Messagesummarize
StartTimesummarize
Total_TransactionIdsummarize
TransactionIDsummarize
Urisummarize

Armorblox Needs Review Alert

#

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

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

'This rule generates an alert for an Armorblox incident where the remediation action is "Needs Review".'

Rule Body

id: 322d4765-be6b-4868-9e3f-138a4f339dd6
name: Armorblox Needs Review Alert
description: |
  'This rule generates an alert for an Armorblox incident where the remediation action is "Needs Review".'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Armorblox
    dataTypes:
      - Armorblox_CL
queryFrequency: 10m
queryPeriod: 10m
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Armorblox_CL 
  | where remediation_actions_s contains "Needs Review"
  | extend users_json = parse_json(users_s)
  | extend Name = users_json[0].name, Email = users_json[0].email
  | project-away users_json
entityMappings:
  - entityType: Mailbox
    fieldMappings:
      - identifier: MailboxPrimaryAddress
        columnName: Email
      - identifier: DisplayName
        columnName: Name
eventGroupingSettings:
  aggregationKind: AlertPerResult
alertDetailsOverride:
  alertDisplayNameFormat: Alert from Armorblox
  alertDescriptionFormat: 'Incident {{id_s}} generated at {{date_t}} needs review '
  alertSeverityColumnName: priority_s
customDetails:
  IncidentId: id_s
  RemediationAction: remediation_actions_s
version: 1.0.2
kind: Scheduled

Stages and Predicates

Stage 1: source

Armorblox_CL

Stage 2: where

| where remediation_actions_s contains "Needs Review"

Stage 3: extend

| extend users_json = parse_json(users_s)

Stage 4: extend

| extend Name = users_json[0].name, Email = users_json[0].email

Stage 5: project-away

| project-away users_json

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Emailextend
Nameextend

AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation

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

Identifies creation of IAM policies that grant broad AWS Lambda CRUD permissions and subsequent policy attachment activity to a principal. This behavior may indicate an attempt to add capabilities that support privilege escalation or unauthorized execution pathways.

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 22115d3c-e87c-485a-9130-33797d619124
name: AWSCloudTrail - Creation of CRUD Lambda policy and then privilege escalation
description: |
  Identifies creation of IAM policies that grant broad AWS Lambda CRUD permissions and subsequent policy attachment
  activity to a principal. This behavior may indicate an attempt to add capabilities that support privilege
  escalation or unauthorized execution pathways.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
relevantTechniques:
  - T1484
  - T1098.003
query: |
  let EventNameList = dynamic(["AttachUserPolicy","AttachRolePolicy","AttachGroupPolicy"]);
  let createPolicy =  dynamic(["CreatePolicy", "CreatePolicyVersion"]);
  let timeframe = 1d;
  let lookback = 14d;
  // Creating Master table with all the events to use with materialize for better performance
  let EventInfo = AWSCloudTrail
  | where TimeGenerated >= ago(lookback)
  | where EventName in (EventNameList) or EventName in (createPolicy)
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "");
  //Checking for Policy creation event with Full Admin Privileges since lookback period.
  let FullAdminPolicyEvents =  materialize(  EventInfo
  | where TimeGenerated >= ago(lookback)
  | where EventName in (createPolicy)
  | extend PolicyName = tostring(parse_json(RequestParameters).policyName)
  | extend Statement = parse_json(tostring((parse_json(RequestParameters).policyDocument))).Statement
  | mvexpand Statement
  | extend Action = parse_json(Statement).Action , Effect = tostring(parse_json(Statement).Effect), Resource = tostring(parse_json(Statement).Resource), Condition = tostring(parse_json(Statement).Condition)
  | extend Action = tostring(Action)
  | where Effect =~ "Allow" and (Action contains "lambda:Create" and Action contains "lambda:Get" and Action contains "lambda:Update" and Action contains "lambda:Delete") and Resource == "*" and Condition == ""
  | distinct TimeGenerated, EventName, PolicyName, SourceIpAddress, UserIdentityArn, RecipientAccountId, AccountName, AccountUPNSuffix
  | project-rename StartTime = TimeGenerated  );
  let PolicyAttach = materialize(EventInfo
  | where TimeGenerated >= ago(timeframe)
  | where EventName in (EventNameList) and isempty(ErrorCode) and isempty(ErrorMessage)
  | extend PolicyName = tostring(split(tostring(parse_json(RequestParameters).policyArn),"/")[1])
  | summarize AttachEventCount=count(), StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventSource, EventName, UserIdentityType , UserIdentityArn, SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, PolicyName
  | extend AttachEvent = pack("StartTime", StartTime, "EndTime", EndTime, "EventName", EventName, "UserIdentityType",   UserIdentityType, "SourceIpAddress", SourceIpAddress, "AccountName", AccountName, "AccountUPNSuffix", AccountUPNSuffix, "RecipientAccountId", RecipientAccountId, "UserIdentityArn", UserIdentityArn)
  | project EventSource, PolicyName, AttachEvent, RecipientAccountId, AccountName, AccountUPNSuffix, AttachEventCount
  );
  // Joining the list of PolicyNames and checking if it has been attached to any Roles/Users/Groups.
  // These Roles/Users/Groups will be Privileged and can be used by adversaries as pivot point for privilege escalation via multiple ways.
  FullAdminPolicyEvents
  | join kind=leftouter
  (
      PolicyAttach
  )
  on PolicyName
  | project-away PolicyName1
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  PolicyName: PolicyName
  EventName: EventName
  RecipientAccountId: RecipientAccountId
  UserIdentityArn: UserIdentityArn
alertDetailsOverride:
  alertDisplayNameFormat: 'AWS Lambda privilege escalation policy activity: {{PolicyName}} by {{AccountName}}'
  alertDescriptionFormat: 'Detected {{EventName}} for policy {{PolicyName}} in account {{RecipientAccountId}}.'
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

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

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

Let Binding: PolicyAttach used in Stage 15

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

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

Stage 1: source

AWSCloudTrail

Stage 2: where

| where TimeGenerated >= ago(lookback)

Stage 3: where

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

Stage 4: extend (4 consecutive steps)

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

Stage 5: where

| where TimeGenerated >= ago(lookback)

Stage 6: where

| where EventName in (createPolicy)

Stage 7: extend

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

Stage 8: extend

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

Stage 9: mv-expand

| mvexpand Statement

Stage 10: extend

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

Stage 11: extend

| extend Action = tostring(Action)

Stage 12: where

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

Stage 13: distinct

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

Stage 14: project-rename

| project-rename StartTime = TimeGenerated

Stage 15: join

FullAdminPolicyEvents
| join kind=leftouter
(
    PolicyAttach
)
on PolicyName

Stage 16: project-away

project-away PolicyName1

Stage 17: summarize aggregation inside the join branch

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

Indicators

These rows show field, operator, and value matches.

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

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountNamesummarize
AccountUPNSuffixsummarize
EventNamesummarize
EventSourcesummarize
PolicyNamesummarize
RecipientAccountIdsummarize
SourceIpAddresssummarize
UserIdentityArnsummarize
UserIdentityTypesummarize

AWSCloudTrail - NRT Login to AWS Management Console without MFA

#
Status
available
Severity
low
Group by
AWSRegion, AccountName, AccountUPNSuffix, EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, SessionMfaAuthenticated, SourceIpAddress, UserAgent, UserIdentityAccountId, UserIdentityPrincipalid, UserIdentityUserName
Source
github.com/Azure/Azure-Sentinel

Multi-Factor Authentication (MFA) helps you to prevent credential compromise. This alert identifies logins to the AWS Management Console without MFA. You can limit this detection to trigger for administrative accounts if you do not have MFA enabled on all accounts. This is done by looking at the eventName ConsoleLogin and if the AdditionalEventData field indicates MFA was NOT used and the ResponseElements field indicates NOT a Failure. Thereby indicating that a non-MFA login was successful.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Persistence
Privilege Escalation
Stealth

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 0ee2aafb-4500-4e36-bcb1-e90eec2f0b9b
name: AWSCloudTrail - NRT Login to AWS Management Console without MFA
description: |
  'Multi-Factor Authentication (MFA) helps you to prevent credential compromise. This alert identifies logins to the AWS Management Console without MFA.
  You can limit this detection to trigger for administrative accounts if you do not have MFA enabled on all accounts.
  This is done by looking at the eventName ConsoleLogin and if the AdditionalEventData field indicates MFA was NOT used and the ResponseElements field indicates NOT a Failure. Thereby indicating that a non-MFA login was successful.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
  - connectorId: AWSS3
    dataTypes:
      - AWSCloudTrail
tactics:
  - DefenseEvasion
  - PrivilegeEscalation
  - Persistence
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  AWSCloudTrail
  | where EventName =~ "ConsoleLogin"
  | extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)
  | where MFAUsed !~ "Yes" and LoginResult !~ "Failure"
  | where SessionIssuerUserName !contains "AWSReservedSSO"
  | extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
  | extend UserName = tostring(split(UserIdentityArn, '/')[-1])
  | extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
  | extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
    AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
    UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
      - identifier: CloudAppAccountId
        columnName: RecipientAccountId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
version: 1.0.4
kind: NRT

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName =~ "ConsoleLogin"

Stage 3: extend

| extend MFAUsed = tostring(parse_json(AdditionalEventData).MFAUsed), LoginResult = tostring(parse_json(ResponseElements).ConsoleLogin)

Stage 4: where

| where MFAUsed !~ "Yes" and LoginResult !~ "Failure"

Stage 5: where

| where SessionIssuerUserName !contains "AWSReservedSSO"

Stage 6: extend (4 consecutive steps)

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

Stage 7: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventName, EventTypeName, LoginResult, MFAUsed, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId,  UserIdentityPrincipalid, UserAgent,
  UserIdentityUserName, SessionMfaAuthenticated, SourceIpAddress, AWSRegion

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
SessionIssuerUserNamecontainsAWSReservedSSOexcludes:SessionIssuerUserName field:"SessionIssuerUserName" value:"AWSReservedSSO"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionsummarize
AccountNamesummarize
AccountUPNSuffixsummarize
EndTimeUtcsummarize
EventNamesummarize
EventTypeNamesummarize
LoginResultsummarize
MFAUsedsummarize
RecipientAccountIdsummarize
SessionMfaAuthenticatedsummarize
SourceIpAddresssummarize
StartTimeUtcsummarize
UserAgentsummarize
UserIdentityAccountIdsummarize
UserIdentityPrincipalidsummarize
UserIdentityUserNamesummarize

Box - Abmormal user activity

#
Status
available
Severity
medium
Time window
14d
Group by
SourceName, SrcUserName, User, k
Source
github.com/Azure/Azure-Sentinel

'Detects spikes (deviations from avarage) in user activity.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: 1139230c-cf10-45db-b616-fed0d1415c05
name: Box - Abmormal user activity
description: |
  'Detects spikes (deviations from avarage) in user activity.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BoxDataConnector
    dataTypes:
      - BoxEvents_CL
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let lbperiod_start = 14d;
  let lbperiod_end = 24h;
  let user_actions_1 = BoxEvents
  | where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
  | summarize TotalEvents = count() by SourceName
  | project TotalEvents, User = SourceName;
  let user_actions_2 = BoxEvents
  | where EventEndTime between (ago(lbperiod_start) .. ago(lbperiod_end))
  | summarize TotalEvents = count() by SrcUserName
  | project TotalEvents, User = SrcUserName;
  let TotalActions = (union user_actions_1, user_actions_2)
  | summarize TotalEvents = count() by User
  | extend EventsPerDay = TotalEvents / 29
  | extend k = 1;
  let actions1_last_h = BoxEvents
  | summarize TotalEventsH = count() by SourceName
  | project TotalEventsH, User = SourceName;
  let actions2_last_h = BoxEvents
  | summarize TotalEventsH = count() by SourceName
  | project TotalEventsH, User = SourceName;
  let TotalActionsLastHour = (union actions1_last_h, actions2_last_h)
  | summarize TotalEventsH = sum(TotalEventsH) by User
  | extend k = 1;
  TotalActions
  | join (TotalActionsLastHour) on k
  | where EventsPerDay > TotalEventsH
  | project User
  | extend AccountCustomEntity = User
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let lbperiod_start = 14d;
let lbperiod_end = 24h;

Let Binding: actions1_last_h

let actions1_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;

Let Binding: actions2_last_h

let actions2_last_h = BoxEvents
| summarize TotalEventsH = count() by SourceName
| project TotalEventsH, User = SourceName;

Let Binding: TotalActionsLastHour

let TotalActionsLastHour = (union actions1_last_h, actions2_last_h)
| summarize TotalEventsH = sum(TotalEventsH) by User
| extend k = 1;

Union (2 Sources)

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

Leg 1: user_actions_1

Leg 2: user_actions_2

Applied to the Combined Result

| summarize TotalEvents = count() by User
| extend EventsPerDay = TotalEvents / 29
| extend k = 1 | join (TotalActionsLastHour) on k | where EventsPerDay > TotalEventsH | project User | extend AccountCustomEntity = User

Output Fields

These fields are emitted when the rule matches.

FieldSource
Userproject
AccountCustomEntityextend

Corelight - SMTP Email containing NON Ascii Characters within the Subject

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

'Detects where an emails contain non ascii characters within the Subject.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 50c61708-9824-46f3-87cf-22490796fae2
name: Corelight - SMTP Email containing NON Ascii Characters within the Subject
description: |
  'Detects where an emails contain non ascii characters within the Subject.'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: Corelight
    dataTypes:
      - corelight_smtp
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566 
query: |
  corelight_smtp
  | where subject hasprefix @'\=?utf-16'
  | mv-expand to_recipient = todynamic(to_) to typeof(string)
  | summarize recipients = dcount(to_recipient)
  | extend k = 1
  | join (corelight_smtp
          | where subject hasprefix @'\=?utf-16'
          | mv-expand to_recipient = todynamic(to_) to typeof(string)
          | summarize by to_recipient
          | extend k = 1) on k
  | where recipients > 1
entityMappings:
  - entityType: MailMessage
    fieldMappings:
      - identifier: Recipient
        columnName: to_recipient
version: 2.2.0
kind: Scheduled

Stages and Predicates

Stage 1: source

corelight_smtp

Stage 2: where

| where subject hasprefix @'\=?utf-16'

Stage 3: mv-expand

| mv-expand to_recipient = todynamic(to_) to typeof(string)

Stage 4: summarize

| summarize recipients = dcount(to_recipient)

Stage 5: extend

| extend k = 1

Stage 6: join

| join (corelight_smtp
        | where subject hasprefix @'\=?utf-16'
        | mv-expand to_recipient = todynamic(to_) to typeof(string)
        | summarize by to_recipient
        | extend k = 1) on k

Stage 7: where

where recipients > 1

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
recipientssummarize
kextend

Guardian- Secrets Policy Violation Detection

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

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

Rule Body

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

Stages and Predicates

Stage 1: source

Guardian

Stage 2: where

| where PolicyViolatedControlFeature =~ 'Secrets'

Stage 3: where

| where Severity =~ 'Medium'

Indicators

These rows show field, operator, and value matches.

Mass secret retrieval from Azure Key Vault

#
Status
available
Severity
low
Time window
1d
Group by
CallerObjectId, CallerObjectUPN, ResourceType, ResultType, identity_claim_appid_g
Source
github.com/Azure/Azure-Sentinel

'Identifies mass secret retrieval from Azure Key Vault observed by a single user. Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications. You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 24f8c234-d1ff-40ec-8b73-96b17a3a9c1c
name: Mass secret retrieval from Azure Key Vault
description: |
  'Identifies mass secret retrieval from Azure Key Vault observed by a single user. 
  Mass secret retrival crossing a certain threshold is an indication of credential dump operations or mis-configured applications. 
  You can tweak the EventCountThreshold based on average count seen in your environment and also filter any known sources (IP/Account) and useragent combinations based on historical analysis to further reduce noise'
severity: Low
status: Available
requiredDataConnectors:
  - connectorId: AzureKeyVault
    dataTypes:
      - KeyVaultData
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1003
query: |
  let DistinctSecretsThreshold = 10;
  let EventCountThreshold = 50;
  // To avoid any False Positives, filtering using AppId is recommended.
  // The AppId 509e4652-da8d-478d-a730-e9d4a1996ca4 has been added in the query as it corresponds to Azure Resource Graph performing VaultGet operations for indexing and syncing all tracked resources across Azure.
  // The AppId 8cae6e77-e04e-42ce-b5cb-50d82bce26b1 has been added as it correspond to Microsoft Policy Insights Provider Data Plane performing VaultGet operations for policies checks.
  let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
  let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);
  AzureDiagnostics
  | where OperationName in (OperationList) and ResourceType =~ "VAULTS"
  | where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')
  | extend
      ResourceId,
      ResultType = column_ifexists("ResultType", ""),
      identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
      identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
      identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
      identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")
  | extend
      CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
      CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
  | as _Retrievals
  | where CallerObjectId in (toscalar(
      _Retrievals
      | where ResultType == "Success"
      | summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
      | where Count > DistinctSecretsThreshold
      | summarize make_set(CallerObjectId,10000)
  ))
  | extend
      requestUri_s = column_ifexists("requestUri_s", ""),
      id_s = column_ifexists("id_s", ""),
      CallerIPAddress = column_ifexists("CallerIPAddress", ""),
      clientInfo_s = column_ifexists("clientInfo_s", "")
  | summarize
      EventCount = count(),
      StartTime = min(TimeGenerated),
      EndTime = max(TimeGenerated),
      ResourceList = make_set(Resource, 50),
      OperationNameList = make_set(OperationName, 50),
      RequestURLList = make_set(requestUri_s, 50),
      ResourceId = max(ResourceId),
      CallerIPList = make_set(CallerIPAddress, 50),
      clientInfo_sList = make_set(clientInfo_s, 50),
      CallerIPMax = max(CallerIPAddress)
      by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN
      | where EventCount > EventCountThreshold
  | project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList
  | extend timestamp = EndTime
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: CallerObjectId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: CallerIPMax
version: 1.0.8
kind: Scheduled

Stages and Predicates

Parameters

let DistinctSecretsThreshold = 10;
let EventCountThreshold = 50;
let AllowedAppId = dynamic(["509e4652-da8d-478d-a730-e9d4a1996ca4","8cae6e77-e04e-42ce-b5cb-50d82bce26b1"]);
let OperationList = dynamic(["SecretGet", "KeyGet", "VaultGet"]);

Stage 1: source

AzureDiagnostics

Stage 2: where

| where OperationName in (OperationList) and ResourceType =~ "VAULTS"

Stage 3: where

| where not(identity_claim_appid_g in (AllowedAppId) and OperationName == 'VaultGet')

Stage 4: extend

| extend
    ResourceId,
    ResultType = column_ifexists("ResultType", ""),
    identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g = column_ifexists("identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g", ""),
    identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s = column_ifexists("identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s", ""),
    identity_claim_oid_g = column_ifexists("identity_claim_oid_g", ""),
    identity_claim_upn_s = column_ifexists("identity_claim_upn_s", "")

Stage 5: extend

| extend
    CallerObjectId = iff(isempty(identity_claim_oid_g), identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g, identity_claim_oid_g),
    CallerObjectUPN = iff(isempty(identity_claim_upn_s), identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s, identity_claim_upn_s)
CallerObjectId =
ifisempty(identity_claim_oid_g)identity_claim_http_schemas_microsoft_com_identity_claims_objectidentifier_g
elseidentity_claim_oid_g
CallerObjectUPN =
ifisempty(identity_claim_upn_s)identity_claim_http_schemas_xmlsoap_org_ws_2005_05_identity_claims_upn_s
elseidentity_claim_upn_s

Stage 6: kusto:as

| as _Retrievals

Stage 7: where

| where CallerObjectId in (toscalar(
    _Retrievals
    | where ResultType == "Success"
    | summarize Count = dcount(requestUri_s) by OperationName, CallerObjectId
    | where Count > DistinctSecretsThreshold
    | summarize make_set(CallerObjectId,10000)
))

Stage 8: extend

| extend
    requestUri_s = column_ifexists("requestUri_s", ""),
    id_s = column_ifexists("id_s", ""),
    CallerIPAddress = column_ifexists("CallerIPAddress", ""),
    clientInfo_s = column_ifexists("clientInfo_s", "")

Stage 9: summarize

| summarize
    EventCount = count(),
    StartTime = min(TimeGenerated),
    EndTime = max(TimeGenerated),
    ResourceList = make_set(Resource, 50),
    OperationNameList = make_set(OperationName, 50),
    RequestURLList = make_set(requestUri_s, 50),
    ResourceId = max(ResourceId),
    CallerIPList = make_set(CallerIPAddress, 50),
    clientInfo_sList = make_set(clientInfo_s, 50),
    CallerIPMax = max(CallerIPAddress)
    by ResourceType, ResultType, identity_claim_appid_g, CallerObjectId, CallerObjectUPN

Stage 10: where

| where EventCount > EventCountThreshold

Stage 11: project-reorder

| project-reorder StartTime, EndTime, EventCount, ResourceId,ResourceType,identity_claim_appid_g, CallerObjectId, CallerObjectUPN, ResultType, ResourceList, OperationNameList, RequestURLList, CallerIPList, clientInfo_sList

Stage 12: extend

| extend timestamp = EndTime

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
CallerIPListsummarize
CallerIPMaxsummarize
CallerObjectIdsummarize
CallerObjectUPNsummarize
EndTimesummarize
EventCountsummarize
OperationNameListsummarize
RequestURLListsummarize
ResourceIdsummarize
ResourceListsummarize
ResourceTypesummarize
ResultTypesummarize
StartTimesummarize
clientInfo_sListsummarize
identity_claim_appid_gsummarize
timestampextend

Objects for Protection Group Changed

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

Detects when protection group objects are updated.

Rule Body

id: af97a601-8fac-4628-bdad-5fc0511236b2
name: Objects for Protection Group Changed
description: Detects when protection group objects are updated.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 29140\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    UserName = user,\n  MessageDetails = Description,\n\
  \    Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 29140

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
UserNameproject

Palo Alto Prisma Cloud - Access keys are not rotated for 90 days

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

'Detects access keys which were not rotated for 90 days.'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 777d4993-31bb-4d45-b949-84f58e09fa2f
name: Palo Alto Prisma Cloud - Access keys are not rotated for 90 days
description: |
  'Detects access keys which were not rotated for 90 days.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: PaloAltoPrismaCloud
    dataTypes:
      - PaloAltoPrismaCloud
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1078
query: |
  PaloAltoPrismaCloud
  | where Reason =~ 'NEW_ALERT'
  | where Status =~ 'open'
  | where AlertMessage has 'access keys are not rotated for 90 days'
  | extend AccountCustomEntity = UserName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

PaloAltoPrismaCloud

Stage 2: where

| where Reason =~ 'NEW_ALERT'

Stage 3: where

| where Status =~ 'open'

Stage 4: where

| where AlertMessage has 'access keys are not rotated for 90 days'

Stage 5: extend

| extend AccountCustomEntity = UserName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Pathlock TDnR - SAP Web Dispatcher HTTP Events

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

Detects security-relevant events from SAP Web Dispatcher HTTP logs, forwarded by Pathlock Threat Detection and Response. Web Dispatcher anomalies may indicate web application attacks, request smuggling, unauthorized access to backend SAP systems, or exploitation of SAP Fiori and S/4HANA web frontends.

MITRE ATT&CK Coverage

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c77
name: Pathlock TDnR - SAP Web Dispatcher HTTP Events
kind: Scheduled
description: >-
  Detects security-relevant events from SAP Web Dispatcher HTTP logs, forwarded by Pathlock Threat
  Detection and Response. Web Dispatcher anomalies may indicate web application attacks, request
  smuggling, unauthorized access to backend SAP systems, or exploitation of SAP Fiori and
  S/4HANA web frontends.
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 == "WD_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 == "WD_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

Rare client observed with high reverse DNS lookup count - Static threshold based (ASIM DNS Solution)

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

This rule identifies clients with high reverse DNS counts, which could be carrying out reconnaissance or discovery activity. This helps in detecting the possible initial phases of an attack, like discovery and reconnaissance. It utilizes ASIM normalization and is applied to any source that supports the ASIM DNS schema.

MITRE ATT&CK Coverage

TacticTechniques
Reconnaissance

Telemetry Coverage

ProviderRecord / event type
SysmonEvent ID 22: DNSEvent (DNS query)

Rule Body

id: 77b7c820-5f60-4779-8bdb-f06e21add5f1
name: Rare client observed with high reverse DNS lookup count - Static threshold based (ASIM DNS Solution)
description: |
  'This rule identifies clients with high reverse DNS counts, which could be carrying out reconnaissance or discovery activity. This helps in detecting the possible initial phases of an attack, like discovery and reconnaissance. It utilizes [ASIM](https://aka.ms/AboutASIM) normalization and is applied to any source that supports the ASIM DNS schema.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1d
queryPeriod: 10d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Reconnaissance
relevantTechniques:
  - T1590
query: |
  let threshold = 10;
  let stime = 10d;
  let etime = 1d;
  let SearchDomain = dynamic(["in-addr.arpa"]);
  _Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
  | summarize
    StartTimeUtc = min(TimeGenerated),
    EndTimeUtc = max(TimeGenerated),
    DNSQueryCount=dcount(DnsQuery)
    by SrcIpAddr
  | where DNSQueryCount > threshold
  | project StartTimeUtc, EndTimeUtc, SrcIpAddr, DNSQueryCount 
  | join kind=leftanti (_Im_Dns(starttime=ago(stime), endtime=ago(etime), domain_has_any=SearchDomain)
    | summarize DNSQueryCount=dcount(DnsQuery) by SrcIpAddr, bin(TimeGenerated, 1d)
    | where DNSQueryCount > threshold
    | project SrcIpAddr, DNSQueryCount
    )
    on SrcIpAddr
  | join kind=inner (_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
    | summarize DNSQueries=make_set(DnsQuery, 1000) by SrcIpAddr)
    on SrcIpAddr
  | extend DNSQuerythreshold = threshold
  | project-away SrcIpAddr1
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
eventGroupingSettings:
  aggregationKind: AlertPerResult
customDetails:
  DNSQueries: DNSQueries
  DNSQuerythreshold: DNSQuerythreshold
  DNSQueryCount: DNSQueryCount
alertDetailsOverride:
  alertDisplayNameFormat: "[Static threshold] Rare client has been observed as making high reverse DNS lookup count  - client IP: '{{SrcIpAddr}}'"
  alertDescriptionFormat: "Client identified as making high reverse DNS counts which could be carrying out reconnaissance or discovery activity.\n\nReverse DNS lookup threshold is: '{{DNSQuerythreshold}}'\n\nCurrent reverse DNS lookup count from this client is : '{{DNSQueryCount}}'\n\nDNS queries requested by this client inlcude: '{{DNSQueries}}'"
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 10;
let stime = 10d;
let etime = 1d;
let SearchDomain = dynamic(["in-addr.arpa"]);

Stage 1: source

_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)

Stage 2: summarize

| summarize
  StartTimeUtc = min(TimeGenerated),
  EndTimeUtc = max(TimeGenerated),
  DNSQueryCount=dcount(DnsQuery)
  by SrcIpAddr

Stage 3: where

| where DNSQueryCount > threshold

Stage 4: project

| project StartTimeUtc, EndTimeUtc, SrcIpAddr, DNSQueryCount

Stage 5: join (negated)

join kind=leftanti (_Im_Dns) on SrcIpAddr

Stage 6: join

| join kind=inner (_Im_Dns(starttime=ago(etime), domain_has_any=SearchDomain)
  | summarize DNSQueries=make_set(DnsQuery, 1000) by SrcIpAddr)
  on SrcIpAddr

Stage 7: extend

extend DNSQuerythreshold

Stage 8: project-away

project-away SrcIpAddr1

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DNSQueryCountgt
  • 10 transforms: number
field:"DNSQueryCount" kind:gt value:"10"

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DNSQueryCountgt10excludes:DNSQueryCount field:"DNSQueryCount" value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
DNSQueryCountproject
EndTimeUtcproject
SrcIpAddrproject
StartTimeUtcproject
DNSQuerythresholdextend

Registry Persistence via AppCert DLL Modification

#
Status
available
Severity
medium
Time window
1h
Group by
Computer, Details, EventID, TargetObject
Source
github.com/Azure/Azure-Sentinel

Adversaries may establish persistence and/or elevate privileges by executing malicious content triggered by AppCert DLLs loaded into processes. Dynamic-link libraries (DLLs) that are specified in the AppCertDLLs Registry key under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\ are loaded into every process that calls the ubiquitously used application programming interface (API) functions CreateProcess, CreateProcessAsUser, CreateProcessWithLoginW, CreateProcessWithTokenW, or WinExec. Ref: https://attack.mitre.org/techniques/T1546/009/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: c61ad0ac-ad68-4ebb-b41a-74296d3e0044
name: Registry Persistence via AppCert DLL Modification
description: | 
  'Adversaries may establish persistence and/or elevate privileges by executing malicious content triggered by AppCert DLLs loaded into processes. 
  Dynamic-link libraries (DLLs) that are specified in the AppCertDLLs Registry key under HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\ are loaded into every process that calls the ubiquitously used application programming interface (API) functions CreateProcess, CreateProcessAsUser, CreateProcessWithLoginW, CreateProcessWithTokenW, or WinExec.
  Ref: https://attack.mitre.org/techniques/T1546/009/'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1546.009
query: |
  Event
  | where EventLog =~ "Microsoft-Windows-Sysmon/Operational" and EventID in (13)
  | parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" * 
  | where TargetObject has ("\\Control\\Session Manager\\AppCertDLLs\\")
  | summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer,  TargetObject, Details
  | extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
  - entityType: RegistryKey
    fieldMappings:
      - identifier: Key
        columnName: TargetObject
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.3
kind: Scheduled

Stages and Predicates

Stage 1: source

Event

Stage 2: where

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

Stage 3: parse

| parse EventData with * 'TargetObject">' TargetObject "<" * 'Details">' Details "<" *

Stage 4: where

| where TargetObject has ("\\Control\\Session Manager\\AppCertDLLs\\")

Stage 5: summarize

| summarize StartTimeUtc = min(TimeGenerated), EndTimeUtc = max(TimeGenerated) by EventID, Computer,  TargetObject, Details

Stage 6: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDin
  • 13 corpus 4 (kusto 4)
field:"EventID" kind:in value:"13"
EventLogeq
  • Microsoft-Windows-Sysmon/Operational corpus 7 (kusto 7)
field:"EventLog" kind:eq value:"Microsoft-Windows-Sysmon/Operational"
TargetObjectmatch
  • \Control\Session Manager\AppCertDLLs\ transforms: term
field:"TargetObject" kind:match value:"\Control\Session Manager\AppCertDLLs\"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
Detailssummarize
EndTimeUtcsummarize
EventIDsummarize
StartTimeUtcsummarize
TargetObjectsummarize
DnsDomainextend
HostNameextend

Service Accounts Performing Remote PS

#
Status
available
Severity
high
Time window
1h
Group by
AccountDomain, AccountName, DeviceId, DeviceName
Source
github.com/Azure/Azure-Sentinel

Service Accounts Performing Remote PowerShell. The purpose behind this detection is for finding service accounts that are performing remote powershell sessions. There are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts. To accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria. One of the main advantages of this method is that only requires server telemetry, and not the attacking client. The first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:. Random_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13. Random_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.

MITRE ATT&CK Coverage

TacticTechniques
Lateral Movement

Telemetry Coverage

Rule Body

id: d29cc957-0ddb-4d00-8d6f-ad1bb345ff9a
name: Service Accounts Performing Remote PS
description: |
  Service Accounts Performing Remote PowerShell.
  The purpose behind this detection is for finding service accounts that are performing remote powershell sessions.
  There are two phases to the detection: Identify service accounts, Find remote PS cmdlets being ran by these accounts.
  To accomplish this, we utilize DeviceLogonEvents and DeviceEvents to find cmdlets ran that meet the criteria.
  One of the main advantages of this method is that only requires server telemetry, and not the attacking client.
  The first phase relies on the DeviceLogonEvents to determine whether an account is a service account or not, consider the following accounts with logons:.
  Random_user has DeviceLogonEvents with type 2, 3, 7, 10, 11 & 13.
  Random_service_account 'should' only have DeviceLogonEvents with type 3,4 or 5.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceLogonEvents
      - DeviceEvents
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
query: |
  let InteractiveTypes = pack_array(                                  // Declare Interactive logon type names
      'Interactive',
      'CachedInteractive',
      'Unlock',
      'RemoteInteractive',
      'CachedRemoteInteractive',
      'CachedUnlock'
  );
  let WhitelistedCmdlets = pack_array(                                // List of whitelisted commands that don't provide a lot of value
      'prompt',
      'Out-Default',
      'out-lineoutput',
      'format-default',
      'Set-StrictMode',
      'TabExpansion2'
  );
  let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');     // List of accounts that are known to perform this activity in the environment and can be ignored
  DeviceLogonEvents                                                         // Get all logon events...
  | where AccountName !in~ (WhitelistedAccounts)                      // ...where it is not a whitelisted account...
  | where ActionType == "LogonSuccess"                                // ...and the logon was successful...
  | where AccountName !contains "$"                                   // ...and not a machine logon.
  | where AccountName !has "winrm va_"                                // WinRM will have pseudo account names that match this if there is an explicit permission for an admin to run the cmdlet, so assume it is good.
  | extend IsInteractive=(LogonType in (InteractiveTypes))            // Determine if the logon is interactive (True=1,False=0)...
  | summarize HasInteractiveLogon=max(IsInteractive)                  // ...then bucket and get the maximum interactive value (0 or 1)...
              by AccountName                                          // ... by the AccountNames
  | where HasInteractiveLogon == 0                                    // ...and filter out all accounts that had an interactive logon.
  // At this point, we have a list of accounts that we believe to be service accounts
  // Now we need to find RemotePS sessions that were spawned by those accounts
  // Note that we look at all powershell cmdlets executed to form a 29-day baseline to evaluate the data on today
  | join kind=rightsemi (                                             // Start by dropping the account name and only tracking the...
  	DeviceEvents                                                      // ...
  	| where ActionType == 'PowerShellCommand'                         // ...PowerShell commands seen...
  	| where InitiatingProcessFileName =~ 'wsmprovhost.exe'            // ...whose parent was wsmprovhost.exe (RemotePS Server)...
      | extend AccountName = InitiatingProcessAccountName             // ...and add an AccountName field so the join is easier
  ) on AccountName
  // At this point, we have all of the commands that were ran by service accounts
  | extend Command = tostring(extractjson('$.Command', tostring(AdditionalFields))) // Extract the actual PowerShell command that was executed
  | where Command !in (WhitelistedCmdlets)                                          // Remove any values that match the whitelisted cmdlets
  | summarize (Timestamp, ReportId)=arg_max(TimeGenerated, ReportId),               // Then group all of the cmdlets and calculate the min/max times of execution...
      make_set(Command, 100000), count(), min(TimeGenerated) by                     // ...as well as creating a list of cmdlets ran and the count..
      AccountName, AccountDomain, DeviceName, DeviceId                                             // ...and have the commonality be the account, DeviceName and DeviceId
  // At this point, we have machine-account pairs along with the list of commands run as well as the first/last time the commands were ran
  | order by AccountName asc                                        // Order the final list by AccountName just to make it easier to go through
  | extend HostName = iff(DeviceName has '.', substring(DeviceName, 0, indexof(DeviceName, '.')), DeviceName)
  | extend DnsDomain = iff(DeviceName has '.', substring(DeviceName, indexof(DeviceName, '.') + 1), "")                                           
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountName
      - identifier: DnsDomain
        columnName: AccountDomain
      - identifier: Name
        columnName: AccountName
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let WhitelistedAccounts = pack_array('FakeWhitelistedAccount');

Let Binding: InteractiveTypes used in Stage 6

let InteractiveTypes = pack_array(
    'Interactive',
    'CachedInteractive',
    'Unlock',
    'RemoteInteractive',
    'CachedRemoteInteractive',
    'CachedUnlock'
);

Let Binding: WhitelistedCmdlets used in Stage 11

let WhitelistedCmdlets = pack_array(
    'prompt',
    'Out-Default',
    'out-lineoutput',
    'format-default',
    'Set-StrictMode',
    'TabExpansion2'
);

Stage 1: source

DeviceLogonEvents

Stage 2: where

| where AccountName !in~ (WhitelistedAccounts)

Stage 3: where

| where ActionType == "LogonSuccess"

Stage 4: where

| where AccountName !contains "$"

Stage 5: where

| where AccountName !has "winrm va_"

Stage 6: extend

| extend IsInteractive=(LogonType in (InteractiveTypes))

Stage 7: summarize

| summarize HasInteractiveLogon=max(IsInteractive)
            by AccountName

Stage 8: where

| where HasInteractiveLogon == 0

Stage 9: join

| join kind=rightsemi (
	DeviceEvents
	| where ActionType == 'PowerShellCommand'
	| where InitiatingProcessFileName =~ 'wsmprovhost.exe'
    | extend AccountName = InitiatingProcessAccountName
) on AccountName

Stage 10: extend

extend Command

Stage 11: where

where not (Command == "WhitelistedCmdlets")

Stage 12: summarize

summarize ReportId, Timestamp by AccountName, AccountDomain, DeviceName, DeviceId

Stage 13: sort

sort by AccountName

Stage 14: extend

extend HostName
HostName =
ifDeviceName has "."substring(DeviceName, 0, indexof(DeviceName, '.'))
elseDeviceName

Stage 15: extend

extend DnsDomain
DnsDomain =
ifDeviceName has "."substring(DeviceName, (indexof(DeviceName, '.') + 1))
else""

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountDomainsummarize
AccountNamesummarize
DeviceIdsummarize
DeviceNamesummarize
ReportIdsummarize
Timestampsummarize
HostNameextend
DnsDomainextend

Tenant State Changed

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

Detects when tenant state is updated.

Rule Body

id: 71711b77-486c-41d0-9de0-dcc411fa7b05
name: Tenant State Changed
description: Detects when tenant state is updated.
severity: Informational
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "Veeam_GetSecurityEvents\n| where instanceId == 25000\n| project\n    Date\
  \ = format_datetime(TimeGenerated, 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n\
  \    EventId = instanceId,\n    [\"User Name\"] = user,\n  MessageDetails = Description,\n\
  \   Severity = SeverityDescription"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 25000

Stage 3: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
EventIdproject
MessageDetailsproject
Severityproject
User Nameproject

TI Map Domain Entity to DeviceNetworkEvents

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

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

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: 1546f3b3-de8a-4e62-bfea-815422154981
name: TI Map Domain Entity to DeviceNetworkEvents
description: |
  'This query identifies any Domain indicators of compromise (IOCs) from threat intelligence (TI) by searching for matches in DeviceNetworkEvents.'
severity: Medium
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceNetworkEvents
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelIndicators
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelIndicators
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1071
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let DeviceNetworkEvents_ = DeviceNetworkEvents
    | where isnotempty(RemoteUrl)
    | where TimeGenerated >= ago(dt_lookBack)
    | where ActionType !has "ConnectionFailed"
    | extend Domain = tostring(parse_url(tolower(RemoteUrl)).Host)
    | where isnotempty(Domain)
    | project-rename DeviceNetworkEvents_TimeGenerated = TimeGenerated;
  let DeviceNetworkEventDomains = DeviceNetworkEvents_
    | distinct Domain
    | summarize make_list(Domain);
  ThreatIntelIndicators
  | extend IndicatorType = replace(@"\[|\]|\""", "", tostring(split(ObservableKey, ":", 0)))
  | where IndicatorType == "domain-name"
  | extend DomainName = tolower(ObservableValue)
  | where TimeGenerated >= ago(ioc_lookBack)
  | extend IndicatorId = tostring(split(Id, "--")[2])
  | where DomainName in (DeviceNetworkEventDomains)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by Id, ObservableValue
  | where IsActive and (ValidUntil > now() or isempty(ValidUntil))
  | extend Description = tostring(parse_json(Data).description)
  | extend TrafficLightProtocolLevel = tostring(parse_json(AdditionalFields).TLPLevel)
  | where Description !contains_cs "State: inactive;" and Description !contains_cs "State: falsepos;"
  | project-reorder *, IsActive, Tags, TrafficLightProtocolLevel, DomainName, Type
  | join kind=innerunique (DeviceNetworkEvents_) on $left.DomainName == $right.Domain
  | where DeviceNetworkEvents_TimeGenerated < ValidUntil
  | summarize DeviceNetworkEvents_TimeGenerated = arg_max(DeviceNetworkEvents_TimeGenerated, *) by IndicatorId
  | project DeviceNetworkEvents_TimeGenerated, IndicatorId, Url = RemoteUrl, Confidence, Description, Tags, TrafficLightProtocolLevel, ActionType, DeviceId, DeviceName, InitiatingProcessAccountUpn, InitiatingProcessCommandLine, RemoteIP, RemotePort
  | extend Name = tostring(split(InitiatingProcessAccountUpn, '@', 0)[0]), UPNSuffix = tostring(split(InitiatingProcessAccountUpn, '@', 1)[0])
  | extend timestamp = DeviceNetworkEvents_TimeGenerated, UserPrincipalName = InitiatingProcessAccountUpn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

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

Let Binding: DeviceNetworkEvents_ used in Stages 1, 16

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

Let Binding: DeviceNetworkEventDomains used in Stages 2, 9

let DeviceNetworkEventDomains = DeviceNetworkEvents_
  | distinct Domain
  | summarize make_list(Domain);

Stage 1: source

let DeviceNetworkEvents_

Stage 2: source

let DeviceNetworkEventDomains

Stage 3: source

ThreatIntelIndicators

Stage 4: extend

extend IndicatorType

Stage 5: where

where IndicatorType == "domain-name"

Stage 6: extend

extend DomainName

Stage 7: where

where TimeGenerated >= ago(1209600s)

Stage 8: extend

extend IndicatorId

Stage 9: where

where DomainName == "DeviceNetworkEventDomains"

Stage 10: summarize

summarize LatestIndicatorTime by Id, ObservableValue

Stage 11: where

where (isempty(ValidUntil) or ValidUntil > now()) and IsActive == true

Stage 12: extend

extend Description

Stage 13: extend

extend TrafficLightProtocolLevel

Stage 14: where

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

Stage 15: project-reorder

project-reorder

Stage 16: join

join kind=innerunique (DeviceNetworkEvents_) on $left.DomainName, $right.Domain

Stage 17: where

where DeviceNetworkEvents_TimeGenerated < ValidUntil

Stage 18: summarize

summarize DeviceNetworkEvents_TimeGenerated by IndicatorId

Stage 19: project

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

Stage 20: extend

extend Name, UPNSuffix

Stage 21: extend

extend UserPrincipalName, timestamp

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

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

Output Fields

These fields are emitted when the rule matches.

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

Back to all Kusto rules