Detection rules › Kusto

Kusto rules: 03

App Gateway WAF - Scanner Detection

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

'Identifies a match for a Scanner detection user agent 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/Vulnerability_Scanning_Tools'

MITRE ATT&CK Coverage

Rule Body

id: 9b8dd8fd-f192-42eb-84f6-541920400a7a
name: App Gateway WAF - Scanner Detection 
description: | 
  'Identifies a match for a Scanner detection user agent 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/Vulnerability_Scanning_Tools'
severity: High 
status: Available 
requiredDataConnectors: 
  - connectorId: WAF
    dataTypes: 
      - AzureDiagnostics
queryFrequency: 6h 
queryPeriod: 6h 
triggerOperator: gt 
triggerThreshold: 0 
tactics: 
  - DefenseEvasion
  - Execution
  - InitialAccess
  - Reconnaissance
  - Discovery
relevantTechniques:
  - T1548
  - T1203
  - T1190
  - T1595
  - T1046
tags:
  - Scanner Detection
query:  | 
  let Threshold = 3;
  AGWFirewallLogs
  | where Action == "Matched"
  | where Message contains "Found User-Agent associated with security scanner"
  | 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), Message = make_set(Message), Detail_Message = make_set(DetailedMessage), Detail_Data = make_set(DetailedData), 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 contains "Found User-Agent associated with security scanner"

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.

Output Fields

These fields are emitted when the rule matches.

FieldSource
Actionsummarize
ClientIpsummarize
Detail_Datasummarize
Detail_Messagesummarize
EndTimesummarize
Messagesummarize
StartTimesummarize
Total_TransactionIdsummarize
TransactionIDsummarize
Urisummarize

AWS Security Hub - Detect EC2 Security groups allowing unrestricted high-risk ports

#
Status
available
Severity
high
Time window
1h
Group by
AwsAccountId, AwsRegion, AwsSecurityFindingDescription, AwsSecurityFindingId, AwsSecurityFindingTitle, ComplianceSecurityControlId, SecurityGroupARN, SecurityGroupId
Source
github.com/Azure/Azure-Sentinel

This query detects EC2 Security Groups that allow unrestricted (0.0.0.0/0 or ::/0) ingress to high-risk ports using AWS Security Hub control EC2.19 findings. Publicly exposed management, database, and service ports (e.g., RDP 3389, SSH 22, SQL 1433/3306) significantly increase the risk of brute-force attacks, exploitation, and lateral movement.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Discovery
Lateral Movement

Rule Body

id: d2b6fa0f-6a4c-4c48-8c64-5e2e1ac4e7b9
name: AWS Security Hub - Detect EC2 Security groups allowing unrestricted high-risk ports
description: |
  This query detects EC2 Security Groups that allow unrestricted (0.0.0.0/0 or ::/0) ingress to high-risk ports using AWS Security Hub control EC2.19 findings.
  Publicly exposed management, database, and service ports (e.g., RDP 3389, SSH 22, SQL 1433/3306) significantly increase the risk of brute-force attacks, exploitation, and lateral movement.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AWSSecurityHub
    dataTypes:
      - AWSSecurityHubFindings
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - LateralMovement
  - Discovery
relevantTechniques:
  - T1133
  - T1021
  - T1046
tags:
  - AWS Foundational Security Best Practices v1.0.0
  - NIST 800-53 r5
query: |
  let HighRiskPorts = dynamic([3389,20,23,110,143,3306,8080,1433,9200,9300,25,445,135,21,1434,4333,5432,5500,5601,22,3000,5000,8088,8888]);
  AWSSecurityHubFindings
  | where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"
  | where tostring(AwsSecurityFindingGeneratorId) == "security-control/EC2.19"
        or tostring(ComplianceSecurityControlId) == "EC2.19"
  | mv-expand Resource = Resources
  | where tostring(Resource.Type) == "AwsEc2SecurityGroup"
  | extend SGDetails = Resource.Details.AwsEc2SecurityGroup
  | extend IpPermissions = SGDetails.IpPermissions
  | mv-expand Perm = IpPermissions
  | where toint(Perm.FromPort) in (HighRiskPorts)
  | mv-expand Range = Perm.IpRanges
  | where tostring(Range.CidrIp) in ("0.0.0.0/0", "::/0")
  | summarize TimeGenerated = max(TimeGenerated), OpenHighRiskPorts = make_set(tostring(Perm.FromPort))
      by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
         AwsSecurityFindingId, ComplianceSecurityControlId, SecurityGroupId = tostring(SGDetails.GroupId), SecurityGroupARN = tostring(Resource.Id)
  | extend OpenHighRiskPorts = strcat_array(OpenHighRiskPorts, ", ")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AwsAccountId
      - identifier: CloudAppAccountId
        columnName: AwsAccountId
  - entityType: CloudApplication
    fieldMappings:
      - identifier: Name
        columnName: SecurityGroupARN
customDetails:
  ComplianceControlId: ComplianceSecurityControlId
  Region: AwsRegion
  FindingId: AwsSecurityFindingId
  SecurityGroupId: SecurityGroupId
  OpenHighRiskPorts: OpenHighRiskPorts
alertDetailsOverride:
  alertDisplayNameFormat: "EC2 Security group {{SecurityGroupId}} allows unrestricted high-risk ports"
  alertDescriptionFormat: |-
    EC2 Security group {{SecurityGroupId}} allows unrestricted (0.0.0.0/0 or ::/0) ingress to high-risk ports: {{OpenHighRiskPorts}}. Restrict or remove the offending rules.
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let HighRiskPorts = dynamic([3389,20,23,110,143,3306,8080,1433,9200,9300,25,445,135,21,1434,4333,5432,5500,5601,22,3000,5000,8088,8888]);

Stage 1: source

AWSSecurityHubFindings

Stage 2: where

| where RecordState == "ACTIVE" and ComplianceStatus == "FAILED"

Stage 3: where

| where tostring(AwsSecurityFindingGeneratorId) == "security-control/EC2.19"
      or tostring(ComplianceSecurityControlId) == "EC2.19"

Stage 4: mv-expand

| mv-expand Resource = Resources

Stage 5: where

| where tostring(Resource.Type) == "AwsEc2SecurityGroup"

Stage 6: extend

| extend SGDetails = Resource.Details.AwsEc2SecurityGroup

Stage 7: extend

| extend IpPermissions = SGDetails.IpPermissions

Stage 8: mv-expand

| mv-expand Perm = IpPermissions

Stage 9: where

| where toint(Perm.FromPort) in (HighRiskPorts)

Stage 10: mv-expand

| mv-expand Range = Perm.IpRanges

Stage 11: where

| where tostring(Range.CidrIp) in ("0.0.0.0/0", "::/0")

Stage 12: summarize

| summarize TimeGenerated = max(TimeGenerated), OpenHighRiskPorts = make_set(tostring(Perm.FromPort))
    by AwsAccountId, AwsRegion, AwsSecurityFindingTitle, AwsSecurityFindingDescription,
       AwsSecurityFindingId, ComplianceSecurityControlId, SecurityGroupId = tostring(SGDetails.GroupId), SecurityGroupARN = tostring(Resource.Id)

Stage 13: extend

| extend OpenHighRiskPorts = strcat_array(OpenHighRiskPorts, ", ")

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AwsSecurityFindingGeneratorIdeq
  • security-control/EC2.19 transforms: tostring
field:"AwsSecurityFindingGeneratorId" kind:eq value:"security-control/EC2.19"
ComplianceSecurityControlIdeq
  • EC2.19 transforms: tostring
field:"ComplianceSecurityControlId" kind:eq value:"EC2.19"
ComplianceStatuseq
  • FAILED
field:"ComplianceStatus" kind:eq value:"FAILED"
Perm.FromPortin
  • 110 transforms: toint, kusto_dynamic_path
  • 135 transforms: toint, kusto_dynamic_path
  • 143 transforms: toint, kusto_dynamic_path
  • 1433 transforms: toint, kusto_dynamic_path
  • 1434 transforms: toint, kusto_dynamic_path
  • 20 transforms: toint, kusto_dynamic_path
  • 21 transforms: toint, kusto_dynamic_path
  • 22 transforms: toint, kusto_dynamic_path
  • 23 transforms: toint, kusto_dynamic_path
  • 25 transforms: toint, kusto_dynamic_path
  • 3000 transforms: toint, kusto_dynamic_path
  • 3306 transforms: toint, kusto_dynamic_path
  • 3389 transforms: toint, kusto_dynamic_path
  • 4333 transforms: toint, kusto_dynamic_path
  • 445 transforms: toint, kusto_dynamic_path
  • 5000 transforms: toint, kusto_dynamic_path
  • 5432 transforms: toint, kusto_dynamic_path
  • 5500 transforms: toint, kusto_dynamic_path
  • 5601 transforms: toint, kusto_dynamic_path
  • 8080 transforms: toint, kusto_dynamic_path
  • 8088 transforms: toint, kusto_dynamic_path
  • 8888 transforms: toint, kusto_dynamic_path
  • 9200 transforms: toint, kusto_dynamic_path
  • 9300 transforms: toint, kusto_dynamic_path
field:"Perm.FromPort" kind:in
Range.CidrIpin
  • 0.0.0.0/0 transforms: tostring, kusto_dynamic_path
  • ::/0 transforms: tostring, kusto_dynamic_path
field:"Range.CidrIp" kind:in
RecordStateeq
  • ACTIVE
field:"RecordState" kind:eq value:"ACTIVE"
Resource.Typeeq
  • AwsEc2SecurityGroup transforms: tostring, kusto_dynamic_path
field:"Resource.Type" kind:eq value:"AwsEc2SecurityGroup"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AwsAccountIdsummarize
AwsRegionsummarize
AwsSecurityFindingDescriptionsummarize
AwsSecurityFindingIdsummarize
AwsSecurityFindingTitlesummarize
ComplianceSecurityControlIdsummarize
OpenHighRiskPortsextend
SecurityGroupARNsummarize
SecurityGroupIdsummarize
TimeGeneratedsummarize

AWSCloudTrail - EC2 Startup Shell Script Changed

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

Identifies changes to the EC2 startup script. The shell script will be executed as root/SYSTEM every time the specific instances are booted up. ref : https://github.com/RhinoSecurityLabs/pacu/blob/master/pacu/modules/ec2__startup_shell_script/main.py

MITRE ATT&CK Coverage

Telemetry Coverage

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: f8577e4d-8481-437b-a94e-06f615985668
name: AWSCloudTrail - EC2 Startup Shell Script Changed
description: |
  Identifies changes to the EC2 startup script. The shell script will be executed as root/SYSTEM every time the specific instances are booted up. ref : https://github.com/RhinoSecurityLabs/pacu/blob/master/pacu/modules/ec2__startup_shell_script/main.py
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: AWS
    dataTypes:
      - AWSCloudTrail
queryFrequency: 15m
queryPeriod: 15m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
 AWSCloudTrail
 | where EventName in~ ("ModifyInstanceAttribute", "CreateLaunchTemplate")
 | extend RequestParametersJson = parse_json(RequestParameters)
 | where tostring(RequestParametersJson.userData) != ""
 | extend userData_ = tostring(RequestParametersJson.userData)
 | extend instanceId_ = tostring(RequestParametersJson.instanceId)
 | project-away SourceSystem, Category, Type, TenantId, EventVersion, SessionIssuerAccountId
 | extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)
 | extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]
 | project TimeGenerated, EventName, UserName, Name, UpnSuffix, instanceId_, AWSRegion, SourceIpAddress, EventSource, UserIdentityArn, EventTypeName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UpnSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIpAddress
customDetails:
  InstanceId: instanceId_
  AWSRegion: AWSRegion
  EventName: EventName
  EventSource: EventSource
  EventType: EventTypeName
alertDetailsOverride:
  alertDisplayNameFormat: 'EC2 startup script changed on {{instanceId_}} by {{UserName}}'
  alertDescriptionFormat: 'EC2 startup script changed on {{instanceId_}} by {{UserName}} in {{AWSRegion}}.'
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

AWSCloudTrail

Stage 2: where

| where EventName in~ ("ModifyInstanceAttribute", "CreateLaunchTemplate")

Stage 3: extend

| extend RequestParametersJson = parse_json(RequestParameters)

Stage 4: where

| where tostring(RequestParametersJson.userData) != ""

Stage 5: extend

| extend userData_ = tostring(RequestParametersJson.userData)

Stage 6: extend

| extend instanceId_ = tostring(RequestParametersJson.instanceId)

Stage 7: project-away

| project-away SourceSystem, Category, Type, TenantId, EventVersion, SessionIssuerAccountId

Stage 8: extend

| extend UserName = substring(UserIdentityPrincipalid, indexof_regex(UserIdentityPrincipalid, ":") + 1)

Stage 9: extend

| extend Name = split(UserName,'@')[0],UpnSuffix = split(UserName,'@')[1]

Stage 10: project

| project TimeGenerated, EventName, UserName, Name, UpnSuffix, instanceId_, AWSRegion, SourceIpAddress, EventSource, UserIdentityArn, EventTypeName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventNamein
  • CreateLaunchTemplate
  • ModifyInstanceAttribute
field:"aws::eventName" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
AWSRegionproject
EventNameproject
EventSourceproject
EventTypeNameproject
Nameproject
SourceIpAddressproject
TimeGeneratedproject
UpnSuffixproject
UserIdentityArnproject
UserNameproject
instanceId_project

BitSight - new alert found

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

'Rule helps to detect a new alerts generated in BitSight.'

MITRE ATT&CK Coverage

Rule Body

id: a1275c5e-0ff4-4d15-a7b7-96018cd979f5
name: BitSight - new alert found
description: |
  'Rule helps to detect a new alerts generated in BitSight.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: BitSight
    dataTypes:
      - BitSightAlerts
queryFrequency: 1d
queryPeriod: 24h
triggerOperator: GreaterThan
triggerThreshold: 0
tactics:
  - Impact
  - InitialAccess
relevantTechniques:
  - T1491
  - T1190
query: |
    let timeframe = 24h;
    BitSightAlerts
    | where ingestion_time() > ago(timeframe)
    | extend Severity = case( Severity contains "INCREASE", "Low",
                              Severity contains "WARN" or Severity contains "DECREASE", "Medium",
                              Severity contains "CRITICAL", "High",
                              "Informational")
    | extend CompanyURL = strcat("https://service.bitsighttech.com/app/spm",CompanyURL)
    | project CompanyName, Severity, Trigger, CompanyURL, AlertDate, GUID
incidentConfiguration:
    createIncident: false
eventGroupingSettings:
    aggregationKind: AlertPerResult
alertDetailsOverride:
    alertDisplayNameFormat: 'BitSight: Alert for {{Trigger}} in {{CompanyName}} from bitsight.'
    alertDescriptionFormat: 'Alert generated on {{AlertDate}} in BitSight.\n\nCompany URL: {{CompanyURL}}\nAlert GUID: {{GUID}}'
    alertSeverityColumnName: Severity
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: CompanyURL
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 24h;

Stage 1: source

BitSightAlerts

Stage 2: where

| where ingestion_time() > ago(timeframe)

Stage 3: extend

| extend Severity = case( Severity contains "INCREASE", "Low",
                          Severity contains "WARN" or Severity contains "DECREASE", "Medium",
                          Severity contains "CRITICAL", "High",
                          "Informational")
Severity =
ifSeverity contains "INCREASE""Low"
elif(Severity contains "WARN" or Severity contains "DECREASE")"Medium"
elifSeverity contains "CRITICAL""High"
else"Informational"

Stage 4: extend

| extend CompanyURL = strcat("https://service.bitsighttech.com/app/spm",CompanyURL)

Stage 5: project

| project CompanyName, Severity, Trigger, CompanyURL, AlertDate, GUID

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertDateproject
CompanyNameproject
CompanyURLproject
GUIDproject
Severityproject
Triggerproject

BloodHound Attack Path Finding - Add Key Credential Link Privileges on Tier Zero Objects

#

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
1h
Source
github.com/Azure/Azure-Sentinel

BloodHound Attack Path Finding - Add Key Credential Link Privileges on Tier Zero Objects

Rule Body

id: 34099af9-e79a-4d5a-a50c-c5e51f5bb965
name: BloodHound Attack Path Finding - Add Key Credential Link Privileges on Tier
  Zero Objects
description: BloodHound Attack Path Finding - Add Key Credential Link Privileges on
  Tier Zero Objects
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0AddKeyCredentialLink"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0AddKeyCredentialLink"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Findingeq
  • T0AddKeyCredentialLink
field:"Finding" kind:eq value:"T0AddKeyCredentialLink"

BloodHound Attack Path Finding - DCOM Users on Tier Zero Computers

#

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
1h
Source
github.com/Azure/Azure-Sentinel

BloodHound Attack Path Finding - DCOM Users on Tier Zero Computers

Rule Body

id: c322a41b-c759-4aca-ac9a-25b00791c397
name: BloodHound Attack Path Finding - DCOM Users on Tier Zero Computers
description: BloodHound Attack Path Finding - DCOM Users on Tier Zero Computers
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: BloodHoundEnterprise
    dataTypes:
      - BHEAttackPathsData_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: BHEAttackPathsData_CL | where Finding == "T0DCOM"
entityMappings:
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: domain_name
customDetails:
  Description: ShortDescription
  Remediation: ShortRemediation
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

BHEAttackPathsData_CL

Stage 2: where

| where Finding == "T0DCOM"

Indicators

These rows show field, operator, and value matches.

Claroty - New Asset

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

'Triggers when Claroty reports a new asset event in the environment, indicating that a previously unseen device or system has been discovered and should be reviewed for authorization, ownership, and expected network placement.'

MITRE ATT&CK Coverage

Rule Body

id: 6c29b611-ce69-4016-bf99-eca639fee1f5
name: Claroty - New Asset
description: |
  'Triggers when Claroty reports a new asset event in the environment, indicating that a previously unseen device
  or system has been discovered and should be reviewed for authorization, ownership, and expected network placement.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: CefAma
    dataTypes:
      - CommonSecurityLog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - Discovery
relevantTechniques:
  - T1190
  - T1133
  - T1082
query: |
  ClarotyEvent
  | where EventOriginalType has 'New Asset' or EventType has 'New Asset'
  | extend IPCustomEntity = SrcIpAddr
  | extend AlertTitle = strcat('Claroty new asset detected from ', coalesce(SrcIpAddr, 'unknown source'))
  | extend AlertDescription = strcat('Claroty reported a new asset event from source IP ', coalesce(SrcIpAddr, 'unknown'), '.')
  | project IPCustomEntity, AlertTitle, AlertDescription
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPCustomEntity
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

ClarotyEvent

Stage 2: where

| where EventOriginalType has 'New Asset' or EventType has 'New Asset'

Stage 3: extend (3 consecutive steps)

| extend IPCustomEntity = SrcIpAddr
| extend AlertTitle = strcat('Claroty new asset detected from ', coalesce(SrcIpAddr, 'unknown source'))
| extend AlertDescription = strcat('Claroty reported a new asset event from source IP ', coalesce(SrcIpAddr, 'unknown'), '.')

Stage 4: project

| project IPCustomEntity, AlertTitle, AlertDescription

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AlertDescriptionproject
AlertTitleproject
IPCustomEntityproject

ClientDeniedAccess

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

'Creates an incident in the event a Client has an excessive amounts of denied access requests.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: a9956d3a-07a9-44a6-a279-081a85020cae
name: ClientDeniedAccess
description: |
  'Creates an incident in the event a Client has an excessive amounts of denied access requests.'
severity: Medium
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  let threshold = 15;
  let rejectedAccess = SymantecVIP
  | where isnotempty(RADIUSAuth)
  | where RADIUSAuth =~ "Reject"
  | summarize Total = count() by ClientIP, bin(TimeGenerated, 15m)
  | where Total > threshold
  | project ClientIP;
  SymantecVIP
  | where isnotempty(RADIUSAuth)
  | where RADIUSAuth =~ "Reject"
  | join kind=inner rejectedAccess on ClientIP
  | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), count() by ClientIP, User
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: User
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ClientIP
version: 1.0.4
status: Available
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 15;

Let Binding: rejectedAccess used in Stage 4

let rejectedAccess = SymantecVIP
| where isnotempty(RADIUSAuth)
| where RADIUSAuth =~ "Reject"
| summarize Total = count() by ClientIP, bin(TimeGenerated, 15m)
| where Total > threshold
| project ClientIP;

Stage 1: source

SymantecVIP

Stage 2: where

| where isnotempty(RADIUSAuth)

Stage 3: where

| where RADIUSAuth =~ "Reject"

Stage 4: join

| join kind=inner rejectedAccess on ClientIP

Stage 5: summarize

summarize EndTime, StartTime by ClientIP, User

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
ClientIPsummarize
EndTimesummarize
StartTimesummarize
Usersummarize

Cognni Incidents for Low Sensitivity Governance Information

#

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

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

'Display incidents in which low sensitivity governance information] was placed at risk by user sharing.'

MITRE ATT&CK Coverage

TacticTechniques
Collection

Rule Body

id: d2e40c79-fe8c-428e-8cb9-0e2282d4558c
name: Cognni Incidents for Low Sensitivity Governance Information
description: |
  'Display incidents in which low sensitivity governance information] was placed at risk by user sharing.'
severity: Low
requiredDataConnectors:
  - connectorId: CognniSentinelDataConnector
    dataTypes:
      - CognniIncidents_CL
queryFrequency: 5h
queryPeriod: 5h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1530
query: |
  let lowRisk = 1;
  let governance = 'Governance Information';
  CognniIncidents_CL 
  | where Severity == lowRisk
  | where informationType_s == governance
  | where TimeGenerated >= ago(5h)
  | extend AccountCustomEntity = userId_s
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let lowRisk = 1;
let governance = 'Governance Information';

Stage 1: source

CognniIncidents_CL

Stage 2: where

| where Severity == lowRisk

Stage 3: where

| where informationType_s == governance

Stage 4: where

| where TimeGenerated >= ago(5h)

Stage 5: extend

| extend AccountCustomEntity = userId_s

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend

Guardian- Input Output Relevance Policy Violation Detection

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

'This alert creates an incident when Input Output Relevance Policy Violation detected from the Guardian.'

Rule Body

id: 76fd5899-0dd1-4336-9519-3c2c0d5d6ecb
name: Guardian- Input Output Relevance Policy Violation Detection
description: |
  'This alert creates an incident when Input Output Relevance Policy Violation detected from the Guardian.'
severity: Informational
status: Available
requiredDataConnectors:
  - connectorId: BoschAIShield
    dataTypes:
      - Guardian
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics: []
relevantTechniques: []
query: |
  Guardian
  | where PolicyViolatedControlFeature =~ 'Input Output Relevance'
  | where Severity =~ 'Informational'
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- Input Output Relevance Policy Violation detection
  alertDescriptionFormat: |
    This query detects Input Output Relevance 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 =~ 'Input Output Relevance'

Stage 3: where

| where Severity =~ 'Informational'

Indicators

These rows show field, operator, and value matches.

Illumio VEN Offline Detection Rule

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

'Create Microsoft Sentinel Incident When Ven Goes Into Offline state'

MITRE ATT&CK Coverage

TacticTechniques
Defense Impairment

Rule Body

id: ec07fcd3-724f-426d-9f53-041801ca5f6c
name: Illumio VEN Offline Detection Rule
description: |
  'Create Microsoft Sentinel Incident When Ven Goes Into Offline state'
severity: High
status: Available
requiredDataConnectors: 
  - connectorId: IllumioSaaSDataConnector
    dataTypes:
      - Illumio_Auditable_Events_CL
  - connectorId: SyslogAma
    datatypes:
      - Syslog      
queryFrequency: 60m
queryPeriod: 60m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
- T1562
query: |
    Illumio_Auditable_Events_CL
    | union IllumioSyslogAuditEvents    
    | where event_type has 'agent_offline_check'
    | mv-expand resource_changes
    | extend hostname = resource_changes['resource']['workload']['hostname'],
        workload_href = resource_changes['resource']['workload']['href'],
        workload_labels = resource_changes['resource']['workload']['labels']
    | project-away resource_changes, version, notifications, action, severity, status // action field will have filtered ip addr, so no point of using IP entity
eventGroupingSettings:
  aggregationKind: SingleAlert
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: hostname     
alertDetailsOverride:
  alertDisplayNameFormat: | 
    Illumio VEN Offline Incident for {{hostname}}
  alertDescriptionFormat: | 
    Illumio VEN Offline Incident for {{hostname}} generated at {{TimeGenerated}}
version: 1.0.7
kind: Scheduled

Stages and Predicates

Stage 1: source

Illumio_Auditable_Events_CL

Stage 2: union

union of 1 branches

Stage 3: source

IllumioSyslogAuditEvents

Stage 4: where

where event_type has "agent_offline_check"

Stage 5: mv-expand

mv-expand resource_changes

Stage 6: extend

extend hostname, workload_href, workload_labels

Stage 7: project-away

project-away action, notifications, resource_changes, severity, status, version

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
event_typematch
  • agent_offline_check transforms: term
field:"event_type" kind:match value:"agent_offline_check"

Output Fields

These fields are emitted when the rule matches.

FieldSource
hostnameextend
workload_hrefextend
workload_labelsextend

Jira - New site admin user

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

'Detects new site admin user.'

MITRE ATT&CK Coverage

TacticTechniques
Persistence
Privilege Escalation

Rule Body

id: b894593a-2b4c-4573-bc47-78715224a6f5
name: Jira - New site admin user
description: |
  'Detects new site admin user.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: JiraAuditAPI
    dataTypes:
      - JiraAudit
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1078
query: |
  JiraAudit
  | where EventMessage =~ 'User added to group'
  | where ObjectItemName =~ 'site-admins'
  | extend user = todynamic(AssociatedItems)[0]['name']
  | extend AccountCustomEntity = user
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

JiraAudit

Stage 2: where

| where EventMessage =~ 'User added to group'

Stage 3: where

| where ObjectItemName =~ 'site-admins'

Stage 4: extend

| extend user = todynamic(AssociatedItems)[0]['name']

Stage 5: extend

| extend AccountCustomEntity = user

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
userextend
AccountCustomEntityextend

License Grace Period Started

#
Status
available
Severity
high
Time window
3h
Group by
$left.Edition, $left.Type, $right.Edition, $right.Type
Source
github.com/Azure/Azure-Sentinel

Detects when a Veeam license grace period starts. This might indicate potential licensing issues that need attention.

Rule Body

id: e4828d99-bb06-40b3-8f9d-0f68fb61e9ee
name: License Grace Period Started
description: Detects when a Veeam license grace period starts. This might indicate
  potential licensing issues that need attention.
severity: High
status: Available
requiredDataConnectors:
- connectorId: Syslog
  dataTypes:
  - Syslog
- connectorId: SyslogAma
  dataTypes:
  - Syslog
queryFrequency: 3h
queryPeriod: 3h
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: AlertPerResult
tactics: []
relevantTechniques: []
query: "let license_editions_lookup = union isfuzzy=true (datatable(Edition:string,\
  \ EditionDescription:string)[]), (_GetWatchlist(\"license_editions_lookup\")); \n\
  let license_types_lookup = union isfuzzy=true (datatable(Type:string, TypeDescription:string)[]),\
  \ (_GetWatchlist(\"license_types_lookup\")); \nVeeam_GetSecurityEvents\n| where\
  \ instanceId == 24060\n| extend Edition = extract(\"Edition=\\\"([^\\\"]*)\\\"\"\
  , 1, SyslogMessage)\n| lookup kind=leftouter (license_editions_lookup) \n    on\
  \ $left.Edition == $right.Edition\n| extend Type = extract(\"Type=\\\"([^\\\"]*)\\\
  \"\", 1, SyslogMessage)\n| lookup kind=leftouter (license_types_lookup) \n    on\
  \ $left.Type == $right.Type\n| extend TenantID = extract(\"TenantID=\\\"([^\\\"\
  ]*)\\\"\", 1, SyslogMessage)\n| extend DaysLeft = extract(\"DaysLeft=\\\"([^\\\"\
  ]*)\\\"\", 1, SyslogMessage)\n| extend SupportLeft = extract(\"SupportLeft=\\\"\
  ([^\\\"]*)\\\"\", 1, SyslogMessage)\n| project\n    Date = format_datetime(TimeGenerated,\
  \ 'dd.MM.yyyy HH:mm'),\n    DataSource = original_host,\n    EventId = instanceId,\n\
  \    [\"License Edition\"] = EditionDescription,\n    [\"License Type\"] = TypeDescription,\n\
  \    [\"Days Left\"] = DaysLeft,\n    [\"Days of Support Left\"] = SupportLeft,\n\
  \    MessageDetails = Description,\n    Severity = SeverityDescription\n"
version: 1.0.1
kind: Scheduled
customDetails:
  Date: Date
  VbrHostName: DataSource
  EventId: EventId
  MessageDetails: MessageDetails
  Severity: Severity

Stages and Predicates

Parameters

let license_editions_lookup = union isfuzzy=true (datatable(Edition:string, EditionDescription:string)[]), (_GetWatchlist("license_editions_lookup"));
let license_types_lookup = union isfuzzy=true (datatable(Type:string, TypeDescription:string)[]), (_GetWatchlist("license_types_lookup"));

Stage 1: source

Veeam_GetSecurityEvents

Stage 2: where

| where instanceId == 24060

Stage 3: extend

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

Stage 4: kusto:lookup

lookup kind=leftouter (license_editions_lookup) on $left.Edition, $right.Edition

Stage 5: extend

extend Type

Stage 6: kusto:lookup

lookup kind=leftouter (license_types_lookup) on $left.Type, $right.Type

Stage 7: extend (3 consecutive steps)

extend DaysLeft, SupportLeft, TenantID

Stage 8: project

project DataSource, Date, Days Left, Days of Support Left, EventId, License Edition, License Type, MessageDetails, Severity

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DataSourceproject
Dateproject
Days Leftproject
Days of Support Leftproject
EventIdproject
License Editionproject
License Typeproject
MessageDetailsproject
Severityproject

Office ASR rule triggered from browser spawned office process.

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

The attacker sends a spearphishing email to a user. The email contains a link which points to a website that eventually presents the user a download of an MS Office document. This document contains a malicious macro. The macro triggers one of the ASR rules. This detection looks for Office ASR violations triggered by an Office document opened from a browser. Note: be aware that you need to have the proper ASR rules enabled for this detection to work.

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Telemetry Coverage

Rule Body

id: 30580043-2451-4d35-b49f-065728529f4a
name: Office ASR rule triggered from browser spawned office process.  
description: |
  The attacker sends a spearphishing email to a user. The email contains a link which points to a website that eventually presents the user a download of an MS Office document. This document contains a malicious macro. The macro triggers one of the ASR rules. 
  This detection looks for Office ASR violations triggered by an Office document opened from a browser.
  Note: be aware that you need to have the proper ASR rules enabled for this detection to work. 
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceEvents
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566.002
query: |
  // Add your own browsers here as well. 
  let browsers = dynamic(["iexplore.exe", "chrome.exe", "firefox.exe", "msedge.exe"]);
  DeviceEvents
  | where ActionType contains "Office"
  | where InitiatingProcessFileName in~ (browsers) or InitiatingProcessParentFileName in~ (browsers)
  // Ignore docx, xlsx and pptx files. These don't contain macros.
  | where not(FileName endswith ".docx" or FileName endswith ".xlsx" or FileName endswith ".pptx")
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: AccountSid 
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: ProcessCommandLine
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let browsers = dynamic(["iexplore.exe", "chrome.exe", "firefox.exe", "msedge.exe"]);

Stage 1: source

DeviceEvents

Stage 2: where

| where ActionType contains "Office"

Stage 3: where

| where InitiatingProcessFileName in~ (browsers) or InitiatingProcessParentFileName in~ (browsers)

Stage 4: where

| where not(FileName endswith ".docx" or FileName endswith ".xlsx" or FileName endswith ".pptx")

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
FileNameends_with.docxexcludes:FileName field:"FileName" value:".docx"
FileNameends_with.pptxexcludes:FileName field:"FileName" value:".pptx"
FileNameends_with.xlsxexcludes:FileName field:"FileName" value:".xlsx"

Pathlock TDnR - J2EE Security Audit Events

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

Detects events from the SAP J2EE (Java) security audit log, forwarded by Pathlock Threat Detection and Response. Security audit events from the Java stack may reveal authentication failures, authorization violations, or exploitation attempts targeting SAP NetWeaver Application Server Java.

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c36
name: Pathlock TDnR - J2EE Security Audit Events
kind: Scheduled
description: >-
  Detects events from the SAP J2EE (Java) security audit log, forwarded by Pathlock Threat Detection
  and Response. Security audit events from the Java stack may reveal authentication failures,
  authorization violations, or exploitation attempts targeting SAP NetWeaver Application Server Java.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1082
query: |
  Pathlock_TDnR_CL
  | where DataSource == "J2EE_SEC_AUD_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 == "J2EE_SEC_AUD_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

Pathlock TDnR - LDAP Synchronization Application Log Events

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

Detects security events from the SAP LDAP synchronization application log (SLG1), forwarded by Pathlock Threat Detection and Response. LDAP sync anomalies may indicate unauthorized identity directory manipulation, credential harvesting via LDAP queries, or synchronization abuse to propagate unauthorized access.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 2a3b4c5d-6e7f-4a0b-8c1d-2e3f4a5b6c58
name: Pathlock TDnR - LDAP Synchronization Application Log Events
kind: Scheduled
description: >-
  Detects security events from the SAP LDAP synchronization application log (SLG1),
  forwarded by Pathlock Threat Detection and Response. LDAP sync anomalies may indicate
  unauthorized identity directory manipulation, credential harvesting via LDAP queries,
  or synchronization abuse to propagate unauthorized access.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: Pathlock_TDnR
    dataTypes:
      - Pathlock_TDnR_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1552
query: |
  Pathlock_TDnR_CL
  | where DataSource == "SLG1_LDAPSYNC"
  | project TimeGenerated, Sysid, DataSource, Eventid, Instance, Hostname, Bname,
            Tcode, Report, Area, Subid, SrcIp, DestIp, AffectedUser, LogLine,
            MsgType, MsgId, MsgNo, MessageV1, MessageV2, MessageV3, MessageV4, CentralTs
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Bname
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIp
eventGroupingSettings:
  aggregationKind: SingleAlert
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: 5h
    matchingMethod: AnyAlert
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []
suppressionDuration: 5h
suppressionEnabled: false
version: 1.0.0

Stages and Predicates

Stage 1: source

Pathlock_TDnR_CL

Stage 2: where

| where DataSource == "SLG1_LDAPSYNC"

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

Threat Essentials - Time series anomaly for data size transferred to public internet

#
Status
available
Severity
medium
Time window
14d
Group by
AnomalyHour, DeviceVendor, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score
Source
github.com/Azure/Azure-Sentinel

'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern. A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated. The higher the score, the further it is from the baseline value. The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour. The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed . You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'

MITRE ATT&CK Coverage

TacticTechniques
Exfiltration

Rule Body

id: b49a1093-cbf6-4973-89ac-2eef98f533c6
name: Threat Essentials - Time series anomaly for data size transferred to public internet
description: |
  'Identifies anomalous data transfer to public networks. The query leverages built-in KQL anomaly detection algorithms that detects large deviations from a baseline pattern.
  A sudden increase in data transferred to unknown public networks is an indication of data exfiltration attempts and should be investigated.
  The higher the score, the further it is from the baseline value.
  The output is aggregated to provide summary view of unique source IP to destination IP address and port bytes sent traffic observed in the flagged anomaly hour.
  The source IP addresses which were sending less than bytessentperhourthreshold have been exluded whose value can be adjusted as needed .
  You may have to run queries for individual source IP addresses from SourceIPlist to determine if anything looks suspicious'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: CiscoASA
    dataTypes:
      - CommonSecurityLog
  - connectorId: CiscoAsaAma
    dataTypes:
      - CommonSecurityLog
  - connectorId: PaloAltoNetworks
    dataTypes:
      - CommonSecurityLog
  - connectorId: AzureMonitor(VMInsights)
    dataTypes:
      - VMConnection
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 1
tactics:
  - Exfiltration
relevantTechniques:
  - T1030
tags:
  - DEV-0537
query: |
  let starttime = 14d;
  let endtime = 1d;
  let timeframe = 1h;
  let scorethreshold = 5;
  let bytessentperhourthreshold = 10;
  let PrivateIPregex = @'^127\.|^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.';
  let TimeSeriesData = (union isfuzzy=true
  (
  VMConnection
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where isnotempty(DestinationIp) and isnotempty(SourceIp)
  | extend DestinationIpType = iff(DestinationIp matches regex PrivateIPregex,"private" ,"public" )
  | where DestinationIpType == "public" | extend DeviceVendor = "VMConnection"
  | project TimeGenerated, BytesSent, DeviceVendor
  | make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
  ),
  (
  CommonSecurityLog
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | extend DestinationIpType = iff(DestinationIP matches regex PrivateIPregex,"private" ,"public" )
  | where DestinationIpType == "public"
  | project TimeGenerated, SentBytes, DeviceVendor
  | make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor
  )
  );
  //Filter anomolies against TimeSeriesData
  let TimeSeriesAlerts = materialize(TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(TotalBytesSent, scorethreshold, -1, 'linefit')
  | mv-expand TotalBytesSent to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double),score to typeof(double), baseline to typeof(long)
  | where anomalies > 0 | extend AnomalyHour = TimeGenerated
  | extend TotalBytesSentinMBperHour = round(((TotalBytesSent / 1024)/1024),2), baselinebytessentperHour = round(((baseline / 1024)/1024),2), score = round(score,2)
  | project DeviceVendor, AnomalyHour, TimeGenerated, TotalBytesSentinMBperHour, baselinebytessentperHour, anomalies, score);
  let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
  //Union of all BaseLogs aggregated per hour
  let BaseLogs = (union isfuzzy=true
  (
  CommonSecurityLog
  | where isnotempty(DestinationIP) and isnotempty(SourceIP)
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | extend DestinationIpType = iff(DestinationIP matches regex PrivateIPregex,"private" ,"public" )
  | where DestinationIpType == "public"
  | extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
  | summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
  | where TotalSentBytesinMB > bytessentperhourthreshold
  | sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
  | extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
  | where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
  | project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
  ),
  (
  VMConnection
  | where isnotempty(DestinationIp) and isnotempty(SourceIp)
  | where TimeGenerated > ago(2d)
  | extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
  | where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
  | extend SourceIP = SourceIp, DestinationIP = DestinationIp
  | extend DestinationIpType = iff(DestinationIp matches regex PrivateIPregex,"private" ,"public" )
  | where DestinationIpType == "public" | extend DeviceVendor = "VMConnection"
  | extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
  | summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
  | where TotalSentBytesinMB > bytessentperhourthreshold
  | sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
  | extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour) // Ranking the dataset per Hourly Partition
  | where Rank < 10  // Selecting Top 10 records with Highest BytesSent in each Hour
  | project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
  )
  );
  // Join against base logs to retrive records associated with the hour of anomoly
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join (
      BaseLogs | extend AnomalyHour = TimeGeneratedHour
  ) on DeviceVendor, AnomalyHour | sort by score desc
  | project DeviceVendor, AnomalyHour,TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
  | summarize EventCount = count(), StartTimeUtc= min(TimeGeneratedMax), EndTimeUtc= max(TimeGeneratedMax), SourceIPMax= arg_max(SourceIP,*), TotalBytesSentinMB = sum(TotalSentBytesinMB), TotalBytesReceivedinMB = sum(TotalReceivedBytesinMB), SourceIPList = make_set(SourceIP, 100), DestinationIPList = make_set(DestinationIPList, 100) by AnomalyHour,TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies
  | project DeviceVendor, AnomalyHour, StartTimeUtc, EndTimeUtc, SourceIPMax, SourceIPList, DestinationIPList, DestinationPortList, TotalBytesSentinMB, TotalBytesReceivedinMB, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies, EventCount
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIPMax
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let scorethreshold = 5;
let bytessentperhourthreshold = 10;

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

Let Binding: PrivateIPregex used in Stages 5, 13

let PrivateIPregex = @'^127\.|^10\.|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-1]\.|^192\.168\.';

Let Binding: AnomalyHours

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

Let Binding: BaseLogs used in Stage 24

let BaseLogs = (union isfuzzy=true
(
CommonSecurityLog
| where isnotempty(DestinationIP) and isnotempty(SourceIP)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| extend DestinationIpType = iff(DestinationIP matches regex PrivateIPregex,"private" ,"public" )
| where DestinationIpType == "public"
| extend SentBytesinMB = ((SentBytes / 1024)/1024), ReceivedBytesinMB = ((ReceivedBytes / 1024)/1024)
| summarize HourlyCount = count(), TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort,100), TotalSentBytesinMB = sum(SentBytesinMB), TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
),
(
VMConnection
| where isnotempty(DestinationIp) and isnotempty(SourceIp)
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| extend SourceIP = SourceIp, DestinationIP = DestinationIp
| extend DestinationIpType = iff(DestinationIp matches regex PrivateIPregex,"private" ,"public" )
| where DestinationIpType == "public" | extend DeviceVendor = "VMConnection"
| extend SentBytesinMB = ((BytesSent / 1024)/1024), ReceivedBytesinMB = ((BytesReceived / 1024)/1024)
| summarize HourlyCount = count(),TimeGeneratedMax=arg_max(TimeGenerated, *), DestinationIPList=make_set(DestinationIP, 100), DestinationPortList = make_set(DestinationPort, 100), TotalSentBytesinMB = sum(SentBytesinMB),TotalReceivedBytesinMB = sum(ReceivedBytesinMB) by SourceIP, DeviceVendor, TimeGeneratedHour=bin(TimeGenerated,1h)
| where TotalSentBytesinMB > bytessentperhourthreshold
| sort by TimeGeneratedHour asc, TotalSentBytesinMB desc
| extend Rank=row_number(1, prev(TimeGeneratedHour) != TimeGeneratedHour)
| where Rank < 10
| project DeviceVendor, TimeGeneratedHour, TimeGeneratedMax, SourceIP, DestinationIPList, DestinationPortList, TotalSentBytesinMB, TotalReceivedBytesinMB, Rank
)
);

Stages 1 to 22 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 23 to 28 use it.

Stage 1: union

union of 2 branches

Stage 2: source

VMConnection

Stage 3: where

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

Stage 4: where

| where isnotempty(DestinationIp) and isnotempty(SourceIp)

Stage 5: extend

| extend DestinationIpType = iff(DestinationIp matches regex PrivateIPregex,"private" ,"public" )
DestinationIpType =
ifDestinationIp matches regex "PrivateIPregex""private"
else"public"

Stage 6: where

| where DestinationIpType == "public"

Stage 7: extend

| extend DeviceVendor = "VMConnection"

Stage 8: project

| project TimeGenerated, BytesSent, DeviceVendor

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

Stage 9: make-series

| make-series TotalBytesSent=sum(BytesSent) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor

Stage 10: source

CommonSecurityLog

Stage 11: where

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

Stage 12: where

| where isnotempty(DestinationIP) and isnotempty(SourceIP)

Stage 13: extend

| extend DestinationIpType = iff(DestinationIP matches regex PrivateIPregex,"private" ,"public" )
DestinationIpType =
ifDestinationIP matches regex "PrivateIPregex""private"
else"public"

Stage 14: where

| where DestinationIpType == "public"

Stage 15: project

| project TimeGenerated, SentBytes, DeviceVendor

Stage 16: make-series

| make-series TotalBytesSent=sum(SentBytes) on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by DeviceVendor

Stage 17: extend

extend anomalies, baseline, score

Stage 18: mv-expand

mv-expand TotalBytesSent

Stage 19: where

where anomalies > 0

Stage 20: extend

extend AnomalyHour

Stage 21: extend

extend TotalBytesSentinMBperHour, baselinebytessentperHour, score

Stage 22: project

project AnomalyHour, DeviceVendor, TimeGenerated, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score

Stage 23: where

where TimeGenerated > ago(172800s)

Stage 24: join

join (BaseLogs) on DeviceVendor, AnomalyHour

Stage 25: sort

sort by score

Stage 26: project

project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, SourceIP, TimeGeneratedMax, TotalBytesSentinMBperHour, TotalReceivedBytesinMB, TotalSentBytesinMB, anomalies, baselinebytessentperHour, score

Stage 27: summarize

summarize DestinationIPList, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB by AnomalyHour, TotalBytesSentinMBperHour, baselinebytessentperHour, score, anomalies

Stage 28: project

project AnomalyHour, DestinationIPList, DestinationPortList, DeviceVendor, EndTimeUtc, EventCount, SourceIPList, SourceIPMax, StartTimeUtc, TotalBytesReceivedinMB, TotalBytesSentinMB, TotalBytesSentinMBperHour, anomalies, baselinebytessentperHour, score

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
AnomalyHourproject
DestinationIPListproject
DestinationPortListproject
DeviceVendorproject
EndTimeUtcproject
EventCountproject
SourceIPListproject
SourceIPMaxproject
StartTimeUtcproject
TotalBytesReceivedinMBproject
TotalBytesSentinMBproject
TotalBytesSentinMBperHourproject
anomaliesproject
baselinebytessentperHourproject
scoreproject

TI map Email entity to SecurityEvent

#
Severity
medium
Time window
14d
Group by
$left.EmailSenderAddress, $right.TargetUserName, IndicatorId, TargetUserName
Source
github.com/Azure/Azure-Sentinel

'Identifies a match in SecurityEvent table from any Email IOC from TI'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access

Rule Body

id: 2fc5d810-c9cc-491a-b564-841427ae0e50
name: TI map Email entity to SecurityEvent
description: |
  'Identifies a match in SecurityEvent table from any Email IOC from TI'
severity: Medium
requiredDataConnectors:
  - connectorId: ThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: ThreatIntelligenceTaxii
    dataTypes:
      - ThreatIntelligenceIndicator
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvents
  - connectorId: WindowsForwardedEvents
    dataTypes:
      - WindowsEvent
  - connectorId: MicrosoftDefenderThreatIntelligence
    dataTypes:
      - ThreatIntelligenceIndicator
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
relevantTechniques:
  - T1566
query: |
  let dt_lookBack = 1h;
  let ioc_lookBack = 14d;
  let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';
  ThreatIntelligenceIndicator
  //Filtering the table for Email related IOCs
  | where isnotempty(EmailSenderAddress)
  | where TimeGenerated >= ago(ioc_lookBack)
  | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId
  | where Active == true and ExpirationDateTime > now()
  // using innerunique to keep perf fast and result set low, we only need one match to indicate potential malicious activity that needs to be investigated
  | join kind=innerunique (
  (union isfuzzy=true
  (SecurityEvent
  | where TimeGenerated >= ago(dt_lookBack) and isnotempty(TargetUserName)
  //Normalizing the column to lower case for exact match with EmailSenderAddress column
  | extend TargetUserName = tolower(TargetUserName)
  // renaming timestamp column so it is clear the log this came from SecurityEvent table
  | extend SecurityEvent_TimeGenerated = TimeGenerated
  ),
  (WindowsEvent
  | where TimeGenerated >= ago(dt_lookBack)
  | extend TargetUserName = tostring(EventData.TargetUserName)
  | where isnotempty(TargetUserName)
  //Normalizing the column to lower case for exact match with EmailSenderAddress column
  | extend TargetUserName = tolower(TargetUserName)
  // renaming timestamp column so it is clear the log this came from SecurityEvent table
  | extend SecurityEvent_TimeGenerated = TimeGenerated
  ))
  )
  on $left.EmailSenderAddress == $right.TargetUserName
  | where SecurityEvent_TimeGenerated < ExpirationDateTime
  | summarize SecurityEvent_TimeGenerated = arg_max(SecurityEvent_TimeGenerated, *) by IndicatorId, TargetUserName
  | project SecurityEvent_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, Url, ExpirationDateTime, ConfidenceScore,
  EmailSenderName, EmailRecipient, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, FileHashValue, FileHashType, Computer, EventID, TargetUserName, Activity, IpAddress, AccountType,
  LogonTypeName, LogonProcessName, Status, SubStatus
  | extend HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))
  | extend timestamp = SecurityEvent_TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: TargetUserName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain   
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
  - entityType: URL
    fieldMappings:
      - identifier: Url
        columnName: Url
version: 1.3.6
kind: Scheduled

Stages and Predicates

Parameters

let dt_lookBack = 1h;
let ioc_lookBack = 14d;
let emailregex = @'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$';

Stage 1: source

ThreatIntelligenceIndicator

Stage 2: where

| where isnotempty(EmailSenderAddress)

Stage 3: where

| where TimeGenerated >= ago(ioc_lookBack)

Stage 4: summarize

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

Stage 5: where

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

Stage 6: join

join kind=innerunique (...) on $left.EmailSenderAddress, $right.TargetUserName

Stage 7: where

where SecurityEvent_TimeGenerated < ExpirationDateTime

Stage 8: summarize

summarize SecurityEvent_TimeGenerated by IndicatorId, TargetUserName

Stage 9: project

project AccountType, Activity, ActivityGroupNames, Computer, ConfidenceScore, Description, EmailRecipient, EmailSenderName, EmailSourceDomain, EmailSourceIpAddress, EmailSubject, EventID, ExpirationDateTime, FileHashType, FileHashValue, IndicatorId, IpAddress, LogonProcessName, LogonTypeName, SecurityEvent_TimeGenerated, Status, SubStatus, TargetUserName, ThreatType, Url

Stage 10: extend

extend DnsDomain, HostName

Stage 11: extend

extend timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Activeeq
  • true corpus 60 (kusto 60)
field:"Active" kind:eq value:"true"
EmailSenderAddressis_not_null
  • (no value, null check)
field:"EmailSenderAddress" kind:is_not_null
TargetUserNameis_not_null
  • (no value, null check)
field:"TargetUserName" kind:is_not_null

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountTypeproject
Activityproject
ActivityGroupNamesproject
Computerproject
ConfidenceScoreproject
Descriptionproject
EmailRecipientproject
EmailSenderNameproject
EmailSourceDomainproject
EmailSourceIpAddressproject
EmailSubjectproject
EventIDproject
ExpirationDateTimeproject
FileHashTypeproject
FileHashValueproject
IndicatorIdproject
IpAddressproject
LogonProcessNameproject
LogonTypeNameproject
SecurityEvent_TimeGeneratedproject
Statusproject
SubStatusproject
TargetUserNameproject
ThreatTypeproject
Urlproject
DnsDomainextend
HostNameextend
timestampextend

TIE privileged accounts issues

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

'Searches for triggered Indicators of Exposures related to privileged accounts issues.'

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

Rule Body

id: 5c170c73-75ba-48ea-8dfc-e4e2d4f23979
name: TIE privileged accounts issues
description: |
  'Searches for triggered Indicators of Exposures related to privileged accounts issues.'
severity: Low
requiredDataConnectors:
  - connectorId: TenableIE
    dataTypes:
      - Tenable_IE_CL
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
relevantTechniques:
  - T1110
query: |
  // For the query to work properly, make sure you have imported the afad_parser.yaml parser into the workspace
  // Retrieve the parser here: https://aka.ms/sentinel-TenableApp-afad-parser
  // Then, create the Kusto Function with alias afad_parser
  let SeverityTable=datatable(Severity:string,Level:int) [
  "low", 1,
  "medium", 2,
  "high", 3,
  "critical", 4
  ];
  let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-NATIVE-ADM-GROUP-MEMBERS", "C-KRBTGT-PASSWORD", "C-PROTECTED-USERS-GROUP-UNUSED", "C-ADMINCOUNT-ACCOUNT-PROPS", "C-ADM-ACC-USAGE", "C-LAPS-UNSECURE-CONFIG", "C-DISABLED-ACCOUNTS-PRIV-GROUPS"];
  afad_parser
  | where MessageType == 0 and Codename in~ (codeNameList)
  | lookup kind=leftouter SeverityTable on Severity
  | order by Level
  | extend HostName = tostring(split(Host, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Host, '.'), 1, -1), '.'))
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.1
kind: Scheduled

Stages and Predicates

Let Binding: SeverityTable used in Stage 3

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

Let Binding: codeNameList used in Stage 2

let codeNameList = datatable(Codename:string)["C-PRIV-ACCOUNTS-SPN", "C-NATIVE-ADM-GROUP-MEMBERS", "C-KRBTGT-PASSWORD", "C-PROTECTED-USERS-GROUP-UNUSED", "C-ADMINCOUNT-ACCOUNT-PROPS", "C-ADM-ACC-USAGE", "C-LAPS-UNSECURE-CONFIG", "C-DISABLED-ACCOUNTS-PRIV-GROUPS"];

Stage 1: source

afad_parser

Stage 2: where

| where MessageType == 0 and Codename in~ (codeNameList)

Stage 3: kusto:lookup

lookup kind=leftouter (SeverityTable) on Severity

Stage 4: sort

sort by Level

Stage 5: extend

extend DnsDomain, HostName

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsDomainextend
HostNameextend

User Account Created Using Incorrect Naming Format

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

This query looks for accounts being created where the name does not match a defined pattern. Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts. Created accounts should be investigated to ensure they were legitimated created. The user_regex field in the query needs to be populated with the expected pattern for the environment before deployment. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Telemetry Coverage

PlatformRecord / event type
AzureAdd user

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: ee55dc85-d2da-48c1-a6c0-3eaee62a8d56
name: User Account Created Using Incorrect Naming Format
description: |
  'This query looks for accounts being created where the name does not match a defined pattern.
    Attackers may attempt to add accounts as a means of establishing persistant access to an environment, looking for anomalies in created accounts may help identify illegitimately created accounts.
    Created accounts should be investigated to ensure they were legitimated created.
    The user_regex field in the query needs to be populated with the expected pattern for the environment before deployment.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-user-accounts#accounts-not-following-naming-policies'
severity: Low
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1136.003
tags:
  - AADSecOpsGuide
query: |
  // Add the environments expected username format regex below before deploying
  let user_regex = "";
  AuditLogs
  | where OperationName =~ "Add user"
  | where Result =~ "success"
  | extend userAgent = tostring(AdditionalDetails[0].value)
  | extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
  | extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
  | extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
  | extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
  | extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
  | extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
  | extend AddedUser = tostring(TargetResources[0].userPrincipalName)
  | where AddedUser matches regex user_regex
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
  | extend TargetAccountName = tostring(split(AddedUser, "@")[0]), TargetAccountUPNSuffix = tostring(split(AddedUser, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AddedUser
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 1.1.0
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others" ]

Stages and Predicates

Parameters

let user_regex = "";

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName =~ "Add user"

Stage 3: where

| where Result =~ "success"

Stage 4: extend (8 consecutive steps)

| extend userAgent = tostring(AdditionalDetails[0].value)
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIPAddress = tostring(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend AddedUser = tostring(TargetResources[0].userPrincipalName)

Stage 5: where

| where AddedUser matches regex user_regex

Stage 6: extend

| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Stage 7: extend

| extend TargetAccountName = tostring(split(AddedUser, "@")[0]), TargetAccountUPNSuffix = tostring(split(AddedUser, "@")[1])

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
OperationNameeq
  • Add user corpus 4 (kusto 4)
field:"OperationName" kind:eq value:"Add user"
Resulteq
  • success corpus 23 (kusto 23)
field:"Result" kind:eq value:"success"

Output Fields

These fields are emitted when the rule matches.

FieldSource
userAgentextend
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
InitiatingIPAddressextend
InitiatedByextend
AddedUserextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetAccountUPNSuffixextend

Vectra Create Detection Alert for Hosts

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

This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. The intent is to create entries in the SecurityAlert table for every new detection attached to an entity monitored by the Vectra Platform

MITRE ATT&CK Coverage

TacticTechniques
Persistence

Rule Body

id: fb861539-da19-4266-831f-99459b8e7605
name: Vectra Create Detection Alert for Hosts
version: 1.0.1
kind: Scheduled
description: This analytic rule is looking for new attacker behaviors observed by the Vectra Platform. The intent is to create entries in the SecurityAlert table for every new detection attached to an entity monitored by the Vectra Platform
status: Available
requiredDataConnectors:
  - connectorId: VectraXDR
    dataTypes:
      - Detections_Data_CL
severity: Medium
queryFrequency: 10m
queryPeriod: 10m
suppressionDuration: PT1H
suppressionEnabled: false
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Persistence
relevantTechniques:
  - T1546
query: |
  VectraDetections
  | where Type == "host"
  | extend
      entity_uid = ['Entity UID'],
      entity_id = ['Entity ID'],
      entity_type = ['Entity Type'],
      detection_id = ['Detection ID'],
      detection = ['Detection Name'],
      category = ['Detection Category'],
      detection_url = ['Vectra Pivot'],
      mitre = Mitre,
      tags = Tags
  | summarize arg_max(TimeGenerated, *) by ['Detection ID'], entity_uid
entityMappings:
- entityType: Host
  fieldMappings:
  - identifier: HostName
    columnName: entity_uid
incidentConfiguration:
  createIncident: false
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
customDetails:
  entity_id: entity_id
  tags: tags
  entity_type: entity_type
  detection_id: detection_id
  mitre_techniques: mitre
alertDetailsOverride:
  alertDisplayNameFormat: Vectra AI Detection- {{detection}}
  alertDescriptionFormat: Vectra AI has detected {{category}} - {{detection}} on entity {{entity_uid}}.
  alertDynamicProperties:
  - alertProperty: AlertLink
    value: detection_url
eventGroupingSettings:
  aggregationKind: AlertPerResult

Stages and Predicates

Stage 1: source

VectraDetections

Stage 2: where

| where Type == "host"

Stage 3: extend

| extend
    entity_uid = ['Entity UID'],
    entity_id = ['Entity ID'],
    entity_type = ['Entity Type'],
    detection_id = ['Detection ID'],
    detection = ['Detection Name'],
    category = ['Detection Category'],
    detection_url = ['Vectra Pivot'],
    mitre = Mitre,
    tags = Tags

Stage 4: summarize

| summarize arg_max(TimeGenerated, *) by ['Detection ID'], entity_uid

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Typeeq
  • host
field:"Type" kind:eq value:"host"

Output Fields

These fields are emitted when the rule matches.

FieldSource
entity_uidsummarize

Back to all Kusto rules