Detection rules › Kusto

Kusto rules: 05

API - Suspicious Login

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

'42Crunch API protection against suspicious login'

MITRE ATT&CK Coverage

TacticTechniques
Initial Access
Credential Access

Rule Body

id: 7bdc10d6-aa24-4ca9-9a93-802cd8761354
name: API - Suspicious Login
description: |
  '42Crunch API protection against suspicious login'
severity: High
requiredDataConnectors:
  - connectorId: 42CrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
  - connectorId: FortyTwoCrunchAPIProtection
    dataTypes:
      - FortyTwoCrunchAPIProtection
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
eventGroupingSettings:
  aggregationKind: SingleAlert
status: Available
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1110
  - T1190
query: |
  let firstRec = FortyTwoCrunchAPIProtection
  | where TimeGenerated >= ago(5m) 
  | project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid
  | where UriPath has "/api/login?user=" and Status in (200, 403);
  let ipAddressCount = toscalar(firstRec | summarize by SourceIp | count);
  let secondRec = firstRec | summarize arg_max(TimeGenerated, *) by SourceIp;
  let recCount = iff((toscalar(secondRec | count) > 3), ipAddressCount, 0);
  secondRec | take recCount
customDetails:
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: Hostname
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InstanceName
version: 3.0.1
kind: Scheduled

Stages and Predicates

let firstRec and let secondRec are inlined into the numbered stages below.

Let Binding: ipAddressCount

let ipAddressCount = toscalar(firstRec | summarize by SourceIp | count);

Let Binding: recCount used in Stage 6

let recCount = iff((toscalar(secondRec | count) > 3), ipAddressCount, 0);

Stages 1 to 5 define let secondRec as the rule's main pipeline source. Stage 6 uses it.

Stage 1: source

FortyTwoCrunchAPIProtection

Stage 2: where

| where TimeGenerated >= ago(5m)

Stage 3: project-away

| project-away NonBlockingMode, SourcePort, DestinationPort, Query, ApiId, RequestHeader, ResponseHeader, Errors, EventType, Uuid

Stage 4: where

| where UriPath has "/api/login?user=" and Status in (200, 403)

Stage 5: summarize

| summarize arg_max(TimeGenerated, *) by SourceIp

Stage 6: take

secondRec
| take recCount

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Statusin
  • 200
  • 403
field:"Status" kind:in
UriPathmatch
  • /api/login?user= transforms: term
field:"UriPath" kind:match value:"/api/login?user="

Output Fields

These fields are emitted when the rule matches.

FieldSource
SourceIpsummarize

Conditional Access - A new Conditional Access policy was created

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

A new Conditional Access policy was created in Entra ID.

MITRE ATT&CK Coverage

Telemetry Coverage

PlatformRecord / event type
AzureAdd conditional access policy

Rules Detecting the Same Action

These rules filter on the same operation.

Rule Body

id: 0459a1b5-909d-4783-9e27-24536b05a47f
name: Conditional Access - A new Conditional Access policy was created
version: 1.0.1
kind: Scheduled
description: A new Conditional Access policy was created in Entra ID.
severity: Informational
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
relevantTechniques:
  - T1562.007
query: |+
  // A new Conditional Access policy was created.
  AuditLogs
  | where OperationName in ("Add conditional access policy")
  | extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
  | extend accountName = tostring(split(modifiedBy, "@")[0])
  | extend upnSuffix = tostring(split(modifiedBy, "@")[1])
  | project
      TimeGenerated,
      OperationName,
      policy = TargetResources[0].displayName,
      modifiedBy,
      accountName,
      upnSuffix,
      result = Result,
      newPolicy = TargetResources[0].modifiedProperties[0].newValue
  | order by TimeGenerated desc
entityMappings:
- entityType: Account
  fieldMappings:
  - identifier: Name
    columnName: accountName
  - identifier: UPNSuffix
    columnName: upnSuffix
suppressionEnabled: false
suppressionDuration: 5h
eventGroupingSettings:
  aggregationKind: AlertPerResult
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities: []
    groupByAlertDetails: []
    groupByCustomDetails: []

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where OperationName in ("Add conditional access policy")

Stage 3: extend (3 consecutive steps)

| extend modifiedBy = tostring(InitiatedBy.user.userPrincipalName)
| extend accountName = tostring(split(modifiedBy, "@")[0])
| extend upnSuffix = tostring(split(modifiedBy, "@")[1])

Stage 4: project

| project
    TimeGenerated,
    OperationName,
    policy = TargetResources[0].displayName,
    modifiedBy,
    accountName,
    upnSuffix,
    result = Result,
    newPolicy = TargetResources[0].modifiedProperties[0].newValue

Stage 5: sort

| order by TimeGenerated desc

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
OperationNameproject
TimeGeneratedproject
accountNameproject
modifiedByproject
newPolicyproject
policyproject
resultproject
upnSuffixproject

CYFIRMA - Brand Intelligence - Product/Solution Medium Rule

#

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

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

"This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. The IP has been previously associated with hacking activity and web application attacks. Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."

MITRE ATT&CK Coverage

Rule Body

id: 458d964f-d039-4ce0-9741-0b6245ba3374
name: CYFIRMA - Brand Intelligence - Product/Solution Medium Rule
description: |
  "This alert is raised when CYFIRMA detects a critical reputation score for an IP address linked to your infrastructure. 
  The IP has been previously associated with hacking activity and web application attacks. 
  Denied outbound traffic to a foreign country from a known Microsoft data center IP suggests potential misuse or compromise of cloud infrastructure."
version: 1.0.1
kind: Scheduled
severity: Medium
requiredDataConnectors:
  - connectorId: CyfirmaBrandIntelligenceAlertsDC
    dataTypes:
      - CyfirmaBIProductSolutionAlerts_CL
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - ResourceDevelopment
  - InitialAccess
relevantTechniques:
  - T1585.002
  - T1583.001
  - T1566
  - T1583
query: |
  // Medium severity - Product/Solution Impersonation
  let timeFrame = 5m;
  CyfirmaBIProductSolutionAlerts_CL
  | where severity == 'High' and TimeGenerated between (ago(timeFrame) .. now())
  | extend
      Description=description,
      FirstSeen=first_seen,
      LastSeen=last_seen,
      RiskScore=risk_score,
      AlertUID=alert_uid,
      UID=uid,
      AssetType=asset_type,
      AssetValue=asset_value,
      Impact=impact,
      Recommendation=recommendation,
      SourceSype=source_type,
      ProviderName='CYFIRMA',
      ProductName='DeCYFIR/DeTCT'
  | project
      TimeGenerated,
      Description,
      RiskScore,
      FirstSeen,
      LastSeen,
      AlertUID,
      UID,
      AssetType,
      AssetValue,
      Impact,
      Recommendation,
      SourceSype,
      ProductName,
      ProviderName
customDetails:
  AssetType: AssetType
  AssetValue: AssetValue
  Impact: Impact
  Recommendation: Recommendation
  SourceSype: SourceSype
  Description: Description
  RiskScore: RiskScore
  TimeGenerated: TimeGenerated
  FirstSeen: FirstSeen
  LastSeen: LastSeen
  AlertUID: AlertUID
  UID: UID
alertDetailsOverride:
  alertDisplayNameFormat: "CYFIRMA - Brand Impersonation Detected for Product/Solution - Medium Severity - {{AssetType}} : {{AssetValue}} "
  alertDescriptionFormat: "{{Description}} "
  alertDynamicProperties:
    - alertProperty: ProductName
      value: ProductName
    - alertProperty: ProviderName
      value: ProviderName
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: false
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
eventGroupingSettings:
  aggregationKind: AlertPerResult
  

Stages and Predicates

Parameters

let timeFrame = 5m;

Stage 1: source

CyfirmaBIProductSolutionAlerts_CL

Stage 2: where

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

Stage 3: extend

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

Stage 4: project

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

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

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

DumpGuard NTLM challenge detected

#
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

With the DumpGuard tool, attackers are able to dump credetials via Remote Credential Guard on devices that have Credential Guard enabled. The creator of the DumpGuard tool purposely used a hard-coded NTLMv1 challenge into the tool, for easy detection. > [!WARNING] > Since the detection relies on a static IOC that can easily be changed in the source code, this detection has a low confidence score since it can be easily bypassed. However, if the detection hits it is almost 100% certain the alert will be TP. > Also take into account that the NetworkSignatureInspected ActionType in MDE is sampled, which means not very event will be logged.

MITRE ATT&CK Coverage

TacticTechniques
Credential Access

References

Telemetry Coverage

Rule Body

DeviceNetworkEvents
// Get NTLM Challenges
| where ActionType == "NetworkSignatureInspected"
| where tostring(todynamic(AdditionalFields).SignatureName) =~ "NTLM-Challenge"
// Extract the NTLM Sample Packet
| extend SamplePacketContent = extract('\\["(.+)"\\]', 1, tostring(todynamic(AdditionalFields).SamplePacketContent))
// Remove % values, since the '1122334455667788' is easy to find without conversions
| extend NewSamplePacketContent = strcat_array(split(SamplePacketContent, "%"), "")
| where NewSamplePacketContent contains "1122334455667788"

Stages and Predicates

Stage 1: source

DeviceNetworkEvents

Stage 2: where

| where ActionType == "NetworkSignatureInspected"

Stage 3: where

| where tostring(todynamic(AdditionalFields).SignatureName) =~ "NTLM-Challenge"

Stage 4: extend

| extend SamplePacketContent = extract('\\["(.+)"\\]', 1, tostring(todynamic(AdditionalFields).SamplePacketContent))

Stage 5: extend

| extend NewSamplePacketContent = strcat_array(split(SamplePacketContent, "%"), "")

Stage 6: where

| where NewSamplePacketContent contains "1122334455667788"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • NetworkSignatureInspected
field:"ActionType" kind:eq value:"NetworkSignatureInspected"
AdditionalFields.SignatureNameeq
  • NTLM-Challenge transforms: tostring, kusto_accessor_expr:{"expression":"tostring(todynamic(AdditionalFields).SignatureName)","field":"AdditionalFields.SignatureName"}, kusto_dynamic_path
field:"AdditionalFields.SignatureName" kind:eq value:"NTLM-Challenge"
NewSamplePacketContentcontains
  • 1122334455667788
field:"NewSamplePacketContent" kind:contains value:"1122334455667788"

Output Fields

These fields are emitted when the rule matches.

FieldSource
SamplePacketContentextend
NewSamplePacketContentextend

Excessive share permissions

#
Status
available
Severity
medium
Time window
1h
Group by
$left.allowed_principal, $right.identifier, EventData, SourceComputerId, TimeGenerated, system
Source
github.com/Azure/Azure-Sentinel

The query searches for event 5143, which is triggered when a share is created or changed and includes de share permissions. First it checks to see if this is a whitelisted share for the system (e.g. domaincontroller netlogon, printserver print$ etc.). The share permissions are then checked against 'allow' rule (A) for a number of well known overly permissive groups, like all users, guests, authenticated users etc. If these are found, an alert is raised so the share creation may be audited. Note: this rule only checks for changed permissions, to prevent repeat alerts if for example a comment is changed, but the permissions are not altered.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: aba0b08c-aace-40c5-a21d-39153023dcaa
name: Excessive share permissions
description: |
  The query searches for event 5143, which is triggered when a share is created or changed and includes de share permissions.
  First it checks to see if this is a whitelisted share for the system (e.g. domaincontroller netlogon, printserver print$ etc.).
  The share permissions are then checked against 'allow' rule (A) for a number of well known overly permissive groups, like all users, guests, authenticated users etc.
  If these are found, an alert is raised so the share creation may be audited.
  Note: this rule only checks for changed permissions, to prevent repeat alerts if for example a comment is changed, but the permissions are not altered.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Collection
  - Discovery
relevantTechniques:
  - T1039
  - T1135
query: |
  let timeframe=1h;
  let system_roles = datatable(role:string, system:string)                  // Link roles to systems.
    ["DC","dc1.corp.local",
    "DC","dc2.corp.local",
    "PRINT","printer.corp.local"
    ];
  let share_roles = datatable(role:string, share:string)                    // Link roles to shares.
    ["DC", @"\\*\sysvol",
    "DC",@"\\*\netlogon",
    "PRINT",@"\\*\print$"];
  let allowed_system_shares = system_roles                                  // Link systems to shares.
    | join kind=inner share_roles on role
    | extend system = tolower(system), share = tolower(share)
    | project-away role
    | summarize allowed_shares = make_set(share) by system;
  let monitored_principals=datatable(identifier:string, Group_Name:string)  // Define a data-table with groups to monitor.
    ["AN", "Anonymous Logon",            // We accept the 'alias' for these well-known SIDS.
    "AU", "Authenticated Users",
    "BG","Built-in guests",
    "BU","Built-in users",
    "DG","Domain guests",
    "DU","Domain users",
    "WD","Everyone",
    "IU","Interactively Logged-on users",
    "LG","Local Guest",
    "NU","Network logon users",
    "513", "Domain Users",                                                  // Support matching on the last part of a SID.
    "514", "Domain Guests",
    "545", "Builtin Users",
    "546", "Builtin Guests",
    "S-1-5-7", "Anonymous Logon"                                            // For the global SIDS, we accept them as-is.
    ];
  SecurityEvent
  | where TimeGenerated >= ago(timeframe)
  | where EventID == 5143
  | extend EventXML = parse_xml(EventData)
  | extend OldSD = tostring(EventXML["EventData"]["Data"][13]["#text"])     // Grab the previous Security Descriptor.
  | extend NewSD = tostring(EventXML["EventData"]["Data"][14]["#text"])     // Grab the new Security Descriptor.
  | project-away EventXML
  | where tostring(OldSD) !~ tostring(NewSD)                                // Don't bother with unchanged permissions.
  | extend system = tolower(Computer), share=tolower(ShareName)             // Normalize system and share name for matching with whitelist.
  | join kind=leftouter allowed_system_shares on system                     // Retrieve the allowed shares per system.
  | where not(set_has_element(allowed_shares, share))                       // Check if the current share is an allowed share.
  | project-away system, share, allowed_shares                              // Get rid of temporary fields.
  | extend DACLS = extract_all(@"(D:(?:\((?:[\w\-]*;){5}(?:[\w\-]*)\))*)", tostring(NewSD)) // Grab all instances of D:(DACL), in case there are multiple sets.
  | project-away OldSD, NewSD                                               // Get rid of data we no longer need.
  | mv-expand DACLS to typeof(string)                                       // In case there are any duplicate/subsequent D: entries (e.g., D:<dacls>S:<sacls>D:<dacls>) split them out to individual D: sets.
  | extend DACLS = substring(DACLS,2)                                       // Strip the leading D:.
  | extend DACLS = split(DACLS, ")")                                        // Split the sets of DACLS ()() to an array of individual DACLS (). This removes the trailing ) character.
  | mv-expand DACLS to typeof(string)                                       // Duplicate the records in such a way that only 1 DACL per record exist. We will aggregate them back later.
  | extend DACLS = substring(DACLS, 1)                                      // Also remove the leading ( character.
  | where not(isempty(DACLS)) and DACLS startswith "A;"                     // Remove any empty or non-allow DACLs.
  | extend allowed_principal = tostring(split(DACLS,";",5)[0])              // Grab the SID what is affected by this DACL.
  | extend allowed_principal = iff(not(allowed_principal startswith "S-" and string_size(allowed_principal) > 15), allowed_principal, split(allowed_principal,"-",countof(allowed_principal,"-"))[0]) // This line takes only the last part (e.g., 513) of a long SID, so you can refer to groups/users without needing to supply the full SID above.
  | join kind=inner monitored_principals on $left.allowed_principal == $right.identifier // Join the found groups to the table of groups to be monitored above. Adds the more readable 'group_name).
  | project-away allowed_principal, identifier, DACLS
  | summarize Authorized_Public_Principals = make_set(Group_Name), take_any(*) by TimeGenerated, SourceComputerId, EventData // Summarize the fields back, making a set of the various group_name values for this record.
  | project-away Group_Name
  // Begin client-specific filter.
  // End client-specific filter.

entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
version: 1.0.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1h;

Let Binding: system_roles

let system_roles = datatable(role:string, system:string)
  ["DC","dc1.corp.local",
  "DC","dc2.corp.local",
  "PRINT","printer.corp.local"
  ];

Let Binding: share_roles

let share_roles = datatable(role:string, share:string)
  ["DC", @"\\*\sysvol",
  "DC",@"\\*\netlogon",
  "PRINT",@"\\*\print$"];

Let Binding: allowed_system_shares used in Stage 8

let allowed_system_shares = system_roles
  | join kind=inner share_roles on role
  | extend system = tolower(system), share = tolower(share)
  | project-away role
  | summarize allowed_shares = make_set(share) by system;

Let Binding: monitored_principals used in Stage 21

let monitored_principals = datatable(identifier:string, Group_Name:string)
  ["AN", "Anonymous Logon",
  "AU", "Authenticated Users",
  "BG","Built-in guests",
  "BU","Built-in users",
  "DG","Domain guests",
  "DU","Domain users",
  "WD","Everyone",
  "IU","Interactively Logged-on users",
  "LG","Local Guest",
  "NU","Network logon users",
  "513", "Domain Users",
  "514", "Domain Guests",
  "545", "Builtin Users",
  "546", "Builtin Guests",
  "S-1-5-7", "Anonymous Logon"
  ];

Stage 1: source

SecurityEvent

Stage 2: where

| where TimeGenerated >= ago(timeframe)

Stage 3: where

| where EventID == 5143

Stage 4: extend (3 consecutive steps)

| extend EventXML = parse_xml(EventData)
| extend OldSD = tostring(EventXML["EventData"]["Data"][13]["#text"])
| extend NewSD = tostring(EventXML["EventData"]["Data"][14]["#text"])

Stage 5: project-away

| project-away EventXML

Stage 6: where

| where tostring(OldSD) !~ tostring(NewSD)

Stage 7: extend

| extend system = tolower(Computer), share=tolower(ShareName)

Stage 8: join

join kind=leftouter (allowed_system_shares) on system

Stage 9: where

where not (share =~ "allowed_shares")

Stage 10: project-away

project-away allowed_shares, share, system

Stage 11: extend

extend DACLS

Stage 12: project-away

project-away NewSD, OldSD

Stage 13: mv-expand

mv-expand DACLS

Stage 14: extend

extend DACLS

Stage 15: extend

extend DACLS

Stage 16: mv-expand

mv-expand DACLS

Stage 17: extend

extend DACLS

Stage 18: where

where not (isempty(DACLS)) and DACLS startswith "A;"

Stage 19: extend

extend allowed_principal

Stage 20: extend

extend allowed_principal
allowed_principal =
ifnot ((allowed_principal startswith "S-" and allowed_principal > 15))allowed_principal
elsesplit(allowed_principal, "-", countof(allowed_principal, "-"))[0]

Stage 21: join

join kind=inner (monitored_principals) on $left.allowed_principal, $right.identifier

Stage 22: project-away

project-away DACLS, allowed_principal, identifier

Stage 23: summarize

summarize Authorized_Public_Principals by TimeGenerated, SourceComputerId, EventData

Stage 24: project-away

project-away Group_Name

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
shareeqallowed_sharesexcludes:share field:"share" value:"allowed_shares"
DACLSis_null(no value, null check)excludes:DACLS

Output Fields

These fields are emitted when the rule matches.

FieldSource
Authorized_Public_Principalssummarize
EventDatasummarize
SourceComputerIdsummarize
TimeGeneratedsummarize

LDAP reconnaissance via search filters

#
Author
FalconForce
Source
github.com/FalconForceTeam/FalconFriday

This query collects LDAP queries executed on systems monitored by Defender for Endpoint. The filter and distinguished name attributes of these queries are extracted and matched against a list of IOCs obtained from popular reconnaissance tools.

Known False Positives

  • Some legitimate administration tools will also perform LDAP queries that are identified as reconnaissance.

MITRE ATT&CK Coverage

References

Telemetry Coverage

Rule Body

let timeframe = 2*1h;
let suspect_search_filter=dynamic([
    "(&(&(objectCategory=person)(objectClass=user))(|(description=*pass*)(comment=*pass*)))", // Metasploit IOC [1].
    "(&(objectCategory=computer)(operatingSystem=*server*))", // Metasploit IOC [1].
    "(&(objectClass=group))", // Metasploit IOC [1].
    "(&(objectClass=group)(managedBy=*)),(&(objectClass=group)(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))", // Metasploit IOC [1].
    "(&(sAMAccountType=805306369)(dnshostname=*))", // Powerview IOC [1].
    "(&(samAccountType=805306368)(samAccountName=*)", // Powerview IOC [1].
    "(&(samAccountType=805306368)(servicePrincipalName=*)", // Powerview IOC [1].
    "(&(objectClass=msDFS-Linkv2))", // Powerview IOC [1].
    "(&(objectCategory =organizationalUnit)(name=*))" // Powerview IOC [1].
//  "(samAccountType=805306368)" // Empire IOC [1] - disabled by default, since it is too generic.
    "objectClass=trustedDomain", // Recon IOC [2].
//  "objectClass=crossRef", // Recon IOC [2].
    "userAccountControl:1.2.840.113556.1.4.803:=524288", // Recon IOC [2].
    "userAccountControl:1.2.840.113556.1.4.803:=4194304", // AD Recon IOC [2].
    "anr=Remote Desktop Users", // AD Recon IOC [2].
    "defender-tokenData=*", // AD Recon IOC [2].
    "Domain Admins",  // AD Recon IOC, based on own research.
    "Enterprise Admins", // AD Recon IOC, based on own research.
    "CN=Administrators", //  AD Recon IOC, based on own research.
    "(schemaIDGUID=*)", // Sharphound IOC, based on own research.
//  "(objectclass=domain)", // Sharphound IOC, based on own research.
    "(&(objectclass=computer)(userAccountControl:1.2.840.113556.1.4.803:=8192))" // Sharphound IOC, based on own research.
    "(|(samAccountType=805306368)(samAccountType=805306369)(objectclass=organizationalUnit))", // Sharphound IOC, based on own research.
    "(samAccountType=805306368)(samAccountType=805306369)" // Sharphound IOC, based on own research.
]);
let suspect_dn=dynamic([
    "CN=DnsAdmins", // AD Recon IOC, based on own research.
    "CN=Domain Controllers", // Sharphound IOC, based on own research.
    "CN=DnsAdmins", // Sharphound IOC, based on own research.
    "CN=Read-only Domain Controllers", // Sharphound IOC, based on own research.
    "CN=Cloneable Domain Controllers" // Sharphound IOC, based on own research.
]);
DeviceEvents
| where ingestion_time() >= ago(timeframe)
| where ActionType =~ "LdapSearch"
// Begin environment-specific filter.
// End environment-specific filter.
| where not(InitiatingProcessFolderPath startswith @"c:\program files\azure advanced threat protection sensor\" and InitiatingProcessVersionInfoOriginalFileName == @"Microsoft.Tri.Sensor.exe")
| extend AdditionalFields=parse_json(AdditionalFields)
| extend SearchFilter=tostring(AdditionalFields.SearchFilter)
| extend DistinguishedName=tostring(AdditionalFields.DistinguishedName)
| extend AttributeList=tostring(AdditionalFields.AttributeList)
| where SearchFilter has_any(suspect_search_filter) or DistinguishedName has_any(suspect_dn)
| project-reorder DeviceName, InitiatingProcessFolderPath, SearchFilter, DistinguishedName, InitiatingProcessCommandLine
// Begin environment-specific filter.
// End environment-specific filter.
// FileProfile is case-sensitive and works on lower-case hashes.
| extend InitiatingProcessSHA1=tolower(InitiatingProcessSHA1)
| invoke FileProfile(InitiatingProcessSHA1, 1000)
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and InitiatingProcessFileName =~ "microsoft.tri.sensor.exe")
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and SoftwareName =~ "Endpoint Configuration Manager")
| where not(Signer =~ "Microsoft Corporation" and IsCertificateValid == 1 and SoftwareName contains "AD Connect")
| where not(Signer =~ "Microsoft Windows" and IsCertificateValid == 1 and InitiatingProcessFileName =~ "Microsoft.IdentityServer.ServiceHost.exe")
| where not(InitiatingProcessFolderPath =~ @"c:\windows\system32\lsass.exe")
// Begin environment-specific filter.
// End environment-specific filter.

Ngrok Reverse Proxy on Network (ASIM DNS Solution)

#
Status
available
Severity
medium
Time window
1h
Group by
DnsQuery, Domain, Dvc, SrcIpAddr
Source
github.com/Azure/Azure-Sentinel

This detection identifies the top four Ngrok domains from DNS resolution. Ngrok reverse proxy can bypass network defense. While not inherently harmful, it has been used for malicious activities recently.

MITRE ATT&CK Coverage

Telemetry Coverage

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

Rule Body

id: 50b0dfb7-2c94-4eaf-a332-a5936d78c263
name: Ngrok Reverse Proxy on Network (ASIM DNS Solution)
description: |
  'This detection identifies the top four Ngrok domains from DNS resolution. Ngrok reverse proxy can bypass network defense. While not inherently harmful, it has been used for malicious activities recently.'
severity: Medium
status: Available 
tags:
  - Schema: ASimDns
    SchemaVersion: 0.1.6
requiredDataConnectors: []
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1572
  - T1090
  - T1102
query: |
  // Define a list of Ngrok domains
  let NgrokDomains = dynamic(["ngrok.com", "ngrok.io", "ngrok", "tunnel.com", "korgn", "lennut.com"]);
  // Query the _Im_Dns function for the past 1 hour
  _Im_Dns(starttime=ago(1h))
  | where isnotempty(DnsQuery) // Filter out empty DNS queries
  | where DnsQuery has_any (NgrokDomains) // Filter DNS queries that match any of the Ngrok domains
  | summarize Starttime = min(EventStartTime),Endtime=max(EventEndTime),EventsCount=sum(EventCount),EventResults=make_set(EventResult,4) by DnsQuery, Domain, SrcIpAddr, Dvc
  // Summarize the data by Domain, DNS query, source IP address, and device Dvc
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SrcIpAddr
  - entityType: DNS
    fieldMappings:
      - identifier: DomainName
        columnName: Domain
eventGroupingSettings:
  aggregationKind: AlertPerResult
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let NgrokDomains = dynamic(["ngrok.com", "ngrok.io", "ngrok", "tunnel.com", "korgn", "lennut.com"]);

Stage 1: source

_Im_Dns(starttime=ago(1h))

Stage 2: where

| where isnotempty(DnsQuery)

Stage 3: where

| where DnsQuery has_any (NgrokDomains)

Stage 4: summarize

| summarize Starttime = min(EventStartTime),Endtime=max(EventEndTime),EventsCount=sum(EventCount),EventResults=make_set(EventResult,4) by DnsQuery, Domain, SrcIpAddr, Dvc

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
DnsQueryis_not_null
  • (no value, null check)
field:"QueryName" kind:is_not_null
DnsQuerymatch
  • korgn transforms: term
  • lennut.com transforms: term
  • ngrok transforms: term
  • ngrok.com transforms: term
  • ngrok.io transforms: term
  • tunnel.com transforms: term
field:"QueryName" kind:match

Output Fields

These fields are emitted when the rule matches.

FieldSource
DnsQuerysummarize
Domainsummarize
Dvcsummarize
Endtimesummarize
EventResultssummarize
EventsCountsummarize
SrcIpAddrsummarize
Starttimesummarize

Port Scan Detected

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

'This alert creates an incident when a source IP addresses attempt to communicate with a large amount of distinct ports within a short period.'

MITRE ATT&CK Coverage

TacticTechniques
Discovery

Rule Body

id: 427e4c9e-8cf4-4094-a684-a2d060dbca38
name: Port Scan Detected
description: |
  'This alert creates an incident when a source IP addresses attempt to communicate with a large amount of distinct ports within a short period.'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SyslogAma
    datatypes:
      - Syslog
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Discovery
relevantTechniques:
  - T1046
query: |
  let threshold = 50;
  SophosXGFirewall
  | where Log_Type =~ "Firewall"
  | where not(ipv4_is_match("10.0.0.0",Src_IP,8) or ipv4_is_match("172.16.0.0",Src_IP,12) or ipv4_is_match("192.168.0.0",Src_IP,16))
  | summarize dcount(Dst_Port) by Src_IP, bin(TimeGenerated, 5m)
  | where dcount_Dst_Port > threshold
entityMappings:
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: Src_IP
version: 1.0.4
kind: Scheduled

Stages and Predicates

Parameters

let threshold = 50;

Stage 1: source

SophosXGFirewall

Stage 2: where

| where Log_Type =~ "Firewall"

Stage 3: where

| where not(ipv4_is_match("10.0.0.0",Src_IP,8) or ipv4_is_match("172.16.0.0",Src_IP,12) or ipv4_is_match("192.168.0.0",Src_IP,16))

Stage 4: summarize

| summarize dcount(Dst_Port) by Src_IP, bin(TimeGenerated, 5m)

Stage 5: where

| where dcount_Dst_Port > threshold

Indicators

These rows show field, operator, and value matches.

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
Src_IPcidr_match10.0.0.0/8excludes:Src_IP field:"Src_IP" value:"10.0.0.0/8"
Src_IPcidr_match172.16.0.0/12excludes:Src_IP field:"Src_IP" value:"172.16.0.0/12"
Src_IPcidr_match192.168.0.0/16excludes:Src_IP field:"Src_IP" value:"192.168.0.0/16"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Src_IPsummarize

Potential Remote Desktop Tunneling

#
Status
available
Severity
medium
Time window
1h
Group by
Computer, EventID, IpAddress, LogonType, TargetLogonId, TargetUserName
Source
github.com/Azure/Azure-Sentinel

This query detects remote desktop authentication attempts with a localhost source address, which can indicate a tunneled login. Ref: https://www.mandiant.com/resources/bypassing-network-restrictions-through-rdp-tunneling

MITRE ATT&CK Coverage

TacticTechniques
Command & Control

Telemetry Coverage

Rule Body

id: d2e8fd50-8d66-11ec-b909-0242ac120002
name: Potential Remote Desktop Tunneling
description: |
   'This query detects remote desktop authentication attempts with a localhost source address, which can indicate a tunneled login.
   Ref: https://www.mandiant.com/resources/bypassing-network-restrictions-through-rdp-tunneling'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CommandAndControl
relevantTechniques:
  - T1572
query: |
 SecurityEvent
    | where EventID in (4624,4625) and LogonType in (10) and IpAddress in ("::1","127.0.0.1")
    | summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, TargetUserName, TargetLogonId, LogonType, IpAddress
    | extend Name=tostring(split(TargetUserName, "@")[0]), UPNSuffix=tostring(split(TargetUserName, "@")[1])
    | extend HostName = iif(Computer has '.',substring(Computer,0,indexof(Computer,'.')),Computer) , DnsDomain = iif(Computer has '.',substring(Computer,indexof(Computer,'.')+1),'')
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: Name
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IpAddress
version: 1.0.4
kind: Scheduled

Stages and Predicates

Stage 1: source

SecurityEvent

Stage 2: where

| where EventID in (4624,4625) and LogonType in (10) and IpAddress in ("::1","127.0.0.1")

Stage 3: summarize

| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated) by EventID, Computer, TargetUserName, TargetLogonId, LogonType, IpAddress

Stage 4: extend

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

Stage 5: extend

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

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDin
  • 4624 corpus 6 (elastic 3, kusto 3)
  • 4625 corpus 6 (elastic 3, kusto 3)
field:"EventID" kind:in
IpAddressin
  • 127.0.0.1
  • ::1
field:"SourceIp" kind:in
LogonTypein
  • 10 corpus 3 (kusto 3)
field:"LogonType" kind:in value:"10"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Computersummarize
EndTimesummarize
EventIDsummarize
IpAddresssummarize
LogonTypesummarize
StartTimesummarize
TargetLogonIdsummarize
TargetUserNamesummarize
Nameextend
UPNSuffixextend
DnsDomainextend
HostNameextend

Process Execution Frequency Anomaly

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

This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors. The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns. Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity. Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 2c55fe7a-b06f-4029-a5b9-c54a2320d7b8
name: Process Execution Frequency Anomaly
description: |
  'This detection identifies anomalous spike in frequency of executions of sensitive processes which are often leveraged as attack vectors.
  The query leverages KQL's built-in anomaly detection algorithms to find large deviations from baseline patterns.
  Sudden increases in execution frequency of sensitive processes should be further investigated for malicious activity.
  Tune the values from 1.5 to 3 in series_decompose_anomalies for further outliers or based on custom threshold values for score.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Execution
relevantTechniques:
  - T1059
query: |
  let starttime = 14d;
  let endtime = 1d;
  let timeframe = 1h;
  let TotalEventsThreshold = 5;
  // Configure the list with sensitive process names 
  let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);
  let TimeSeriesData =
  SecurityEvent
  | where EventID == 4688 | extend Process = tolower(Process)
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | where Process in~ (ExeList)
  | project TimeGenerated, Computer, AccountType, Account, Process
  | make-series Total=count() on TimeGenerated from startofday(ago(starttime)) to startofday(ago(endtime)) step timeframe by Process;
  let TimeSeriesAlerts = materialize(TimeSeriesData
  | extend (anomalies, score, baseline) = series_decompose_anomalies(Total, 1.5, -1, 'linefit')
  | mv-expand Total to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
  | where anomalies > 0
  | project Process, TimeGenerated, Total, baseline, anomalies, score
  | where Total > TotalEventsThreshold);
  let AnomalyHours = materialize(TimeSeriesAlerts  | where TimeGenerated > ago(2d) | project TimeGenerated);
  TimeSeriesAlerts
  | where TimeGenerated > ago(2d)
  | join (
  SecurityEvent
  | where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
  | 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
  | where EventID == 4688 | extend Process = tolower(Process)
  | summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
  ) on Process, TimeGenerated
  | project AnomalyHour = TimeGenerated, Computer, Account, Process, CommandLine, CommandlineCount, Total, baseline, anomalies, score
  | extend timestamp = AnomalyHour, NTDomain = split(Account, '\\', 0)[0], Name = split(Account, '\\', 1)[0], HostName = tostring(split(Computer, '.', 0)[0]), DnsDomain = tostring(strcat_array(array_slice(split(Computer, '.'), 1, -1), '.'))

entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: Name
      - identifier: NTDomain
        columnName: NTDomain
  - entityType: Host
    fieldMappings: 
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: DnsDomain
version: 1.0.6
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let endtime = 1d;
let timeframe = 1h;
let TotalEventsThreshold = 5;
let ExeList = dynamic(["powershell.exe","cmd.exe","wmic.exe","psexec.exe","cacls.exe","rundll32.exe"]);

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

Let Binding: AnomalyHours used in Stages 3, 17

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

Stages 1 to 15 define let TimeSeriesAlerts as the rule's main pipeline source. Stages 16 to 19 use it.

Stage 1: source

let TimeSeriesData

Stage 2: source

let TimeSeriesAlerts

Stage 3: source

let AnomalyHours

Stage 4: source

SecurityEvent

Stage 5: where

| where EventID == 4688

Stage 6: extend

| extend Process = tolower(Process)

Stage 7: where

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

Stage 8: where

| where Process in~ (ExeList)

Stage 9: project

| project TimeGenerated, Computer, AccountType, Account, Process

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

Stage 10: make-series

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

Stage 11: extend

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

Stage 12: mv-expand

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

Stage 13: where

| where anomalies > 0

Stage 14: project

| project Process, TimeGenerated, Total, baseline, anomalies, score

Stage 15: where

| where Total > TotalEventsThreshold

Stage 16: where

TimeSeriesAlerts
| where TimeGenerated > ago(2d)

Stage 17: join

| join (
SecurityEvent
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(endtime)))
| extend DateHour = bin(TimeGenerated, 1h)
| where DateHour in ((AnomalyHours))
| where EventID == 4688 | extend Process = tolower(Process)
| summarize CommandlineCount = count() by bin(TimeGenerated, 1h), Process, CommandLine, Computer, Account
) on Process, TimeGenerated

Stage 18: project

project Account, AnomalyHour, CommandLine, CommandlineCount, Computer, Process, Total, anomalies, baseline, score

Stage 19: extend

extend DnsDomain, HostName, NTDomain, Name, timestamp

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventIDeq
  • 4688
field:"EventID" kind:eq value:"4688"
Processin
  • cacls.exe
  • cmd.exe
  • powershell.exe
  • psexec.exe
  • rundll32.exe
  • wmic.exe
field:"Process" kind:in
Totalgt
  • 5 transforms: number
field:"Total" kind:gt value:"5"
anomaliesgt
  • 0 transforms: number
field:"anomalies" kind:gt value:"0"

Output Fields

These fields are emitted when the rule matches.

FieldSource
Accountproject
AnomalyHourproject
CommandLineproject
CommandlineCountproject
Computerproject
Processproject
Totalproject
anomaliesproject
baselineproject
scoreproject
DnsDomainextend
HostNameextend
NTDomainextend
Nameextend
timestampextend

Rare Process as a Service

#
Status
available
Severity
medium
Time window
14d
Group by
DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessSHA1, ServiceProcessUser
Source
github.com/Azure/Azure-Sentinel

This query is looking for rarely seen processes which are launched as a service. Whiltelisted process list need to be updated based on the environment. Author: Jouni Mikkola More info: https://threathunt.blog/rare-process-launch-as-a-service/

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 91a451e3-178f-41b2-9e5d-da97d75b9971
name: Rare Process as a Service
description: |
  This query is looking for rarely seen processes which are launched as a service. Whiltelisted process list need to be updated based on the environment.
  Author: Jouni Mikkola
  More info: https://threathunt.blog/rare-process-launch-as-a-service/
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
      - DeviceNetworkEvents
      - DeviceFileEvents
      - DeviceImageLoadEvents
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1543
  - T1543.003
query: |
  let LookupTime = 14d;
  let WhiteList = pack_array(
  "svchost.exe",
  "mssense.exe",
  "msmpeng.exe",
  "searchindexer.exe",
  "microsoftedgeupdate.exe"
  );
  let GetServices = materialize (
  DeviceProcessEvents
  | where TimeGenerated > ago(LookupTime)
  | where InitiatingProcessParentFileName contains "services.exe"
  | where InitiatingProcessFileName !in~(WhiteList)
  | project TimeGenerated, DeviceName, StartedChildProcess = FileName, StartedChildProcessSHA1 = SHA1, StartedChildProcessCmdline = ProcessCommandLine, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName
  );
  GetServices
  | summarize count() by ServiceProcess, DeviceName
  | where count_ < 6 
  | join kind = inner GetServices on ServiceProcess, DeviceName 
  | join kind = leftouter ( 
  DeviceNetworkEvents 
  | where TimeGenerated > ago(LookupTime)
  | where InitiatingProcessParentFileName contains "services.exe"
  | where InitiatingProcessFileName !in~(WhiteList)
  | project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, NetworkAction = ActionType, RemoteIP, RemoteUrl
  ) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
  | join kind = leftouter (
  DeviceFileEvents
  | where TimeGenerated > ago(LookupTime)
  | where InitiatingProcessParentFileName contains "services.exe"
  | where InitiatingProcessFileName !in~(WhiteList)
  | project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, FileAction = ActionType, ModifiedFile = FileName, ModifiedFileSHA1 = SHA1, ModifiedFilePath = FolderPath
  ) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
  | join kind = leftouter (
  DeviceImageLoadEvents
  | where TimeGenerated > ago(LookupTime)
  | where InitiatingProcessParentFileName contains "services.exe"
  | where InitiatingProcessFileName !in~(WhiteList)
  | project TimeGenerated, DeviceName, ServiceProcessSHA1 = InitiatingProcessSHA1, ServiceProcess = InitiatingProcessFileName, ServiceProcessCmdline = InitiatingProcessCommandLine, ServiceProcessID = InitiatingProcessId, ServiceProcessCreationTime = InitiatingProcessCreationTime, ServiceProcessUser = InitiatingProcessAccountName, LoadedDLL = FileName, LoadedDLLSHA1 = SHA1, LoadedDLLPath = FolderPath
  ) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
  | summarize ConnectedAddresses = make_set(RemoteIP, 100000), ConnectedUrls = make_set(RemoteUrl, 100000), FilesModified = make_set(ModifiedFile, 100000),FileModFolderPath = make_set(ModifiedFilePath, 100000),FileModHA1s = make_set(ModifiedFileSHA1, 100000), ChildProcesses = make_set(StartedChildProcess, 100000), ChildCommandlines = make_set(StartedChildProcessCmdline, 100000), DLLsLoaded = make_set(LoadedDLL, 100000), DLLSHA1 = make_set(LoadedDLLSHA1, 100000) by DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1
  | 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: Process
    fieldMappings:
      - identifier: ProcessId
        columnName: ServiceProcessID
      - identifier: CommandLine
        columnName: ServiceProcessCmdline
version: 1.0.0
kind: Scheduled

Stages and Predicates

Parameters

let LookupTime = 14d;

let GetServices is inlined into the numbered stages below.

Let Binding: WhiteList used in Stage 5

let WhiteList = pack_array(
"svchost.exe",
"mssense.exe",
"msmpeng.exe",
"searchindexer.exe",
"microsoftedgeupdate.exe"
);

Stage 1: source

let GetServices

Stage 2: source

DeviceProcessEvents

Stage 3: where

where TimeGenerated > ago(1209600s)

Stage 4: where

where InitiatingProcessParentFileName contains "services.exe"

Stage 5: where

where not (InitiatingProcessFileName =~ "WhiteList")

Stage 6: project

project DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessSHA1, ServiceProcessUser, StartedChildProcess, StartedChildProcessCmdline, StartedChildProcessSHA1, TimeGenerated

Stage 7: summarize

summarize by ServiceProcess, DeviceName

Stage 8: where

where count_ < 6

Stage 9: join

join kind=inner (GetServices) on ServiceProcess, DeviceName

Stage 10: join

join kind=leftouter (DeviceNetworkEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1

Stage 11: join

join kind=leftouter (DeviceFileEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1

Stage 12: join

join kind=leftouter (DeviceImageLoadEvents) on DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1

Stage 13: summarize

summarize ChildCommandlines, ChildProcesses, ConnectedAddresses, ConnectedUrls, DLLSHA1, DLLsLoaded, FileModFolderPath, FileModHA1s, FilesModified by DeviceName, ServiceProcess, ServiceProcessCmdline, ServiceProcessCreationTime, ServiceProcessID, ServiceProcessUser, ServiceProcessSHA1

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.

FieldKindExcluded valuesSearch
InitiatingProcessFileNameeqWhiteListexcludes:InitiatingProcessFileName field:"InitiatingProcessFileName" value:"WhiteList"

Output Fields

These fields are emitted when the rule matches.

FieldSource
ChildCommandlinessummarize
ChildProcessessummarize
ConnectedAddressessummarize
ConnectedUrlssummarize
DLLSHA1summarize
DLLsLoadedsummarize
DeviceNamesummarize
FileModFolderPathsummarize
FileModHA1ssummarize
FilesModifiedsummarize
ServiceProcesssummarize
ServiceProcessCmdlinesummarize
ServiceProcessCreationTimesummarize
ServiceProcessIDsummarize
ServiceProcessSHA1summarize
ServiceProcessUsersummarize
HostNameextend
DnsDomainextend

RSA ID Plus - Locked Administrator Account Detected

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

'Raises an alert when an admin account is locked out of the Admin console (RSAIDPlus Admin Events).'

MITRE ATT&CK Coverage

TacticTechniques
Credential AccessNo specific technique
ImpactNo specific technique

Rule Body

id: "488c759d-a82e-44cd-91bb-d766573918d7"
name: "RSA ID Plus - Locked Administrator Account Detected"
description: |
  'Raises an alert when an admin account is locked out of the Admin console (RSAIDPlus Admin Events).'
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: RSAIDPlus_AdminLogs_Connector
    dataTypes:
      - RSAIDPlus_AdminLogs_CL
queryFrequency: 30m
queryPeriod: 1d
triggerOperator: GreaterThan
triggerThreshold: 0
tactics: 
  - Impact
  - CredentialAccess
relevantTechniques: []
query: |
    RSAIDPlus_AdminLogs_CL
    |where activityKey == 'LOCKED_ADMIN_ACCOUNT'
    |project TimeGenerated, customerName, adminUserName, adminUserRole, activityKey, sourceIPAddress
entityMappings: 
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: sourceIPAddress
  - entityType: Account
    fieldMappings: 
      - identifier: Name
        columnName: adminUserName
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: customerName
incidentConfiguration:
    createIncident: true
    groupingConfiguration : 
      enabled: true
      reopenClosedIncident: false
      lookbackDuration : 2h
      matchingMethod : AllEntities
eventGroupingSettings:
    aggregationKind: SingleAlert
alertDetailsOverride:
    alertDisplayNameFormat: 'RSA ID Plus Administrator - {{{adminUserName}} is locked out.'
    alertDescriptionFormat: 'Administrator - {{{adminUserName}} with Role - {{{adminUserRole}} got locked out of Admin Console at: {{{TimeGenerated}}'
customDetails:
    Customer_Name: customerName
    Administrator: adminUserName
    Administrator_Type: adminUserRole
    Activity: activityKey
version: 1.0.0
kind: Scheduled

Stages and Predicates

Stage 1: source

RSAIDPlus_AdminLogs_CL

Stage 2: where

| where activityKey == 'LOCKED_ADMIN_ACCOUNT'

Stage 3: project

| project TimeGenerated, customerName, adminUserName, adminUserRole, activityKey, sourceIPAddress

Indicators

These rows show field, operator, and value matches.

Output Fields

These fields are emitted when the rule matches.

FieldSource
TimeGeneratedproject
activityKeyproject
adminUserNameproject
adminUserRoleproject
customerNameproject
sourceIPAddressproject

SMB/Windows Admin Shares

#
Status
available
Severity
medium
Time window
1h
Group by
DeviceId, InitiatingProcessFileName, InitiatingProcessId, SHA1
Source
github.com/Azure/Azure-Sentinel

This query is based on detecting incoming RPC/TCP on the SCM, followed by the start of a child process of services.exe. Remotely interacting with the SCM triggers the RPC/TCP traffic on services.exe, and the creation of the child processes is a result of starting the service. The query might look intimidating given its size. That's why we've commented the query per logic block to walk you through the details.

MITRE ATT&CK Coverage

Telemetry Coverage

Rule Body

id: 9da25366-2c77-41a5-a159-0da5e2f5fb90
name: SMB/Windows Admin Shares
description: |
  This query is based on detecting incoming RPC/TCP on the SCM, followed by the start of a child process of services.exe. Remotely interacting with the SCM triggers the RPC/TCP traffic on services.exe, and the creation of the child processes is a result of starting the service. 
  The query might look intimidating given its size. That's why we've commented the query per logic block to walk you through the details.
severity: Medium
status: Available
requiredDataConnectors:
  - connectorId: MicrosoftThreatProtection
    dataTypes:
      - DeviceProcessEvents
      - DeviceNetworkEvents
      - DeviceFileCertificateInfo
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1021.002
query: |
  // This rule is fairly complex, hence this documentation.
  // The rule tries to detect network activity from services.exe followed by the start of a new childvprocess of services.exe (i.e. a service start).
  // The rule tries to filter false positives as much as possible. 
  // Since the FileProfile info is currently super-unreliable (unsure why), we have to work around it.
  // 
  // The following list of LOLBINs is used to include all results which have a high reputation, but are LOLBINs.
  let lolbins = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "Update.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]);
  // First, we want to get all the network events triggered by services.exe.
  let networkEvents = materialize(DeviceNetworkEvents
  | where InitiatingProcessFileName in~ ("services.exe")
  | where ActionType == "InboundConnectionAccepted"
  | project-rename TimestampNetworkAct=Timestamp);
  // Next, we want to get the list of child processes created by services.exe. 
  let allServices = materialize ((
  DeviceProcessEvents 
  // This is for optimization purposes, as filtering is way faster than joins.
  | where DeviceId in ((networkEvents | project DeviceId))
  //svchost and sppsvc are created very often as child processes of services.exe
  | where InitiatingProcessFileName =~ "services.exe" 
  | project TimestampChild=Timestamp, DeviceId, DeviceName, FileName, ProcessCommandLine, SHA1, 
  InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessSHA1, InitiatingProcessId, TimestampServicesExe=InitiatingProcessCreationTime));
  // Now we are going to join the process creations and network events 
  // and filter out all the tables _after_ the join where the child process 
  // of svchost is not created shortly after the network activity (between 0 and 10 seconds). 
  let serviceNetworkEvents = materialize(
  (networkEvents 
      | join kind=inner hint.strategy=shuffle allServices on DeviceId, InitiatingProcessId, InitiatingProcessFileName
  ) 
  | where datetime_diff("Second", TimestampChild, TimestampNetworkAct) between (0 .. 10)
  // Only get the results where the network activity occured more than 1 minute after services.exe has started (i.e. system boot).
  | where datetime_diff("Second", TimestampNetworkAct, TimestampServicesExe) > 60);
  // Next, we want to check the reputation of all procssess.
  // Since FileProfile is not properly working, we use the built-in DeviceFileCertificateInfo for the AntiJoin.
  // The goal is to create a list of SHA1 hashes of the spawned processess 
  // which have a low prevelance and are not in de DeviceFileCertificateInfo.
  let serviceNetworkEventsWithSHA1 = materialize(serviceNetworkEvents
  | summarize count() by SHA1
  | join kind=leftanti hint.strategy=broadcast DeviceFileCertificateInfo on SHA1
  | where count_ < 100);
  // Finally, we need to bring everything together.
  // We take our subset of the child processess created by services.exe (serviceNetworkEvents).
  // Everything which is on our deny-list of SHA1 processess OR are LOLBINs, are filtered out.
  // Additionally, we filter out msiexec, since that appears to come very often.
  serviceNetworkEvents
  | where SHA1 in ((serviceNetworkEventsWithSHA1 | project SHA1)) or FileName in~ (lolbins) 
  | where ProcessCommandLine !~ "msiexec.exe /V" 
  // This summarize is optional if you want to group similar stuff.
  //| summarize ActionType=make_set(ActionType), RemoteIPs=make_set(strcat(RemoteIP, ":", RemotePort, " (", RemoteUrl, ")")), LocalPort=make_set(LocalPort) by bin(TimestampNetworkAct, 1m), DeviceId, DeviceName, LocalIP, Protocol, AdditionalFields, bin(TimestampChild, 1m), FileName, ProcessCommandLine, SHA1, InitiatingProcessFileName, InitiatingProcessCommandLine1, bin(TimestampServicesExe, 1m) 
entityMappings:
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: DeviceName
  - entityType: Account
    fieldMappings:
      - identifier: Sid
        columnName: InitiatingProcessAccountSid 
      - identifier: Name
        columnName: InitiatingProcessAccountName
      - identifier: NTDomain
        columnName: InitiatingProcessAccountDomain
  - entityType: Process
    fieldMappings:
      - identifier: CommandLine
        columnName: InitiatingProcessCommandLine
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: RemoteIP
version: 1.0.0
kind: Scheduled

Stages and Predicates

let networkEvents and let serviceNetworkEvents are inlined into the numbered stages below.

Let Binding: lolbins

let lolbins = dynamic(["At.exe", "Atbroker.exe", "Bash.exe", "Bitsadmin.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Esentutl.exe", "Eventvwr.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "SyncAppvPublishingServer.exe", "Ttdinject.exe", "Tttracer.exe", "vbc.exe", "Verclsid.exe", "Wab.exe", "Wmic.exe", "Wscript.exe", "Wsreset.exe", "Xwizard.exe", "AgentExecutor.exe", "Appvlp.exe", "Bginfo.exe", "Cdb.exe", "csi.exe", "Devtoolslauncher.exe", "dnx.exe", "Dotnet.exe", "Dxcap.exe", "Excel.exe", "Mftrace.exe", "Msdeploy.exe", "msxsl.exe", "ntdsutil.exe", "Powerpnt.exe", "rcsi.exe", "Sqldumper.exe", "Sqlps.exe", "SQLToolsPS.exe", "Squirrel.exe", "te.exe", "Tracker.exe", "Update.exe", "vsjitdebugger.exe", "Winword.exe", "Wsl.exe"]);

Let Binding: allServices used in Stages 2, 9

let allServices = materialize ((
DeviceProcessEvents 
| where DeviceId in ((networkEvents | project DeviceId))
| where InitiatingProcessFileName =~ "services.exe" 
| project TimestampChild=Timestamp, DeviceId, DeviceName, FileName, ProcessCommandLine, SHA1, 
InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessSHA1, InitiatingProcessId, TimestampServicesExe=InitiatingProcessCreationTime));

Let Binding: serviceNetworkEventsWithSHA1 used in Stage 4

let serviceNetworkEventsWithSHA1 = materialize(serviceNetworkEvents
| summarize count() by SHA1
| join kind=leftanti hint.strategy=broadcast DeviceFileCertificateInfo on SHA1
| where count_ < 100);

Stage 1: source

let networkEvents

Stage 2: source

let allServices

Stage 3: source

let serviceNetworkEvents

Stage 4: source

let serviceNetworkEventsWithSHA1

Stage 5: source

DeviceNetworkEvents

Stage 6: where

where InitiatingProcessFileName =~ "services.exe"

Stage 7: where

where ActionType == "InboundConnectionAccepted"

Stage 8: project-rename

project-rename

Stage 9: join

join kind=inner (allServices) on DeviceId, InitiatingProcessId, InitiatingProcessFileName

Stage 10: where TimestampChild - TimestampNetworkAct >= 0s AND TimestampChild - TimestampNetworkAct <= 10s

where TimestampChild - TimestampNetworkAct >= 0s AND TimestampChild - TimestampNetworkAct <= 10s

Stage 11: where TimestampNetworkAct - TimestampServicesExe > 1m

where TimestampNetworkAct - TimestampServicesExe > 1m

Stage 12: where

where (FileName in~ ("AgentExecutor.exe", "Appvlp.exe", "At.exe", "Atbroker.exe", "Bash.exe", "Bginfo.exe", "Bitsadmin.exe", "Cdb.exe", "CertReq.exe", "Certutil.exe", "Cmd.exe", "Cmdkey.exe", "Cmstp.exe", "Control.exe", "Csc.exe", "Cscript.exe", "Desktopimgdownldr.exe", "Devtoolslauncher.exe", "Dfsvc.exe", "Diantz.exe", "Diskshadow.exe", "Dnscmd.exe", "Dotnet.exe", "Dxcap.exe", "Esentutl.exe", "Eventvwr.exe", "Excel.exe", "Expand.exe", "Extexport.exe", "Extrac32.exe", "Findstr.exe", "Forfiles.exe", "Ftp.exe", "GfxDownloadWrapper.exe", "Gpscript.exe", "Hh.exe", "Ie4uinit.exe", "Ieexec.exe", "Ilasm.exe", "Infdefaultinstall.exe", "Installutil.exe", "Jsc.exe", "Makecab.exe", "Mavinject.exe", "Mftrace.exe", "Microsoft.Workflow.Compiler.exe", "Mmc.exe", "MpCmdRun.exe", "Msbuild.exe", "Msconfig.exe", "Msdeploy.exe", "Msdt.exe", "Mshta.exe", "Msiexec.exe", "Netsh.exe", "Odbcconf.exe", "Pcalua.exe", "Pcwrun.exe", "Pktmon.exe", "Powerpnt.exe", "Presentationhost.exe", "Print.exe", "Psr.exe", "Rasautou.exe", "Reg.exe", "Regasm.exe", "Regedit.exe", "Regini.exe", "Register-cimprovider.exe", "Regsvcs.exe", "Regsvr32.exe", "Replace.exe", "Rpcping.exe", "Rundll32.exe", "Runonce.exe", "Runscripthelper.exe", "SQLToolsPS.exe", "Sc.exe", "Schtasks.exe", "Scriptrunner.exe", "Sqldumper.exe", "Sqlps.exe", "Squirrel.exe", "SyncAppvPublishingServer.exe", "Tracker.exe", "Ttdinject.exe", "Tttracer.exe", "Update.exe", "Verclsid.exe", "Wab.exe", "Winword.exe", "Wmic.exe", "Wscript.exe", "Wsl.exe", "Wsreset.exe", "Xwizard.exe", "csi.exe", "dnx.exe", "msxsl.exe", "ntdsutil.exe", "rcsi.exe", "te.exe", "vbc.exe", "vsjitdebugger.exe"))

Stage 13: where

where ProcessCommandLine !~ "msiexec.exe /V"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
ActionTypeeq
  • InboundConnectionAccepted
field:"ActionType" kind:eq value:"InboundConnectionAccepted"
FileNamein
  • AgentExecutor.exe
  • Appvlp.exe
  • At.exe
  • Atbroker.exe
  • Bash.exe
  • Bginfo.exe
  • Bitsadmin.exe
  • Cdb.exe
  • CertReq.exe
  • Certutil.exe
  • Cmd.exe
  • Cmdkey.exe
  • Cmstp.exe
  • Control.exe
  • Csc.exe
  • Cscript.exe
  • Desktopimgdownldr.exe
  • Devtoolslauncher.exe
  • Dfsvc.exe
  • Diantz.exe
  • Diskshadow.exe
  • Dnscmd.exe
  • Dotnet.exe
  • Dxcap.exe
  • Esentutl.exe
  • Eventvwr.exe
  • Excel.exe
  • Expand.exe
  • Extexport.exe
  • Extrac32.exe
  • Findstr.exe
  • Forfiles.exe
  • Ftp.exe
  • GfxDownloadWrapper.exe
  • Gpscript.exe
  • Hh.exe
  • Ie4uinit.exe
  • Ieexec.exe
  • Ilasm.exe
  • Infdefaultinstall.exe
  • +64 more values (see full rule source)
field:"FileName" kind:in
InitiatingProcessFileNameeq
  • services.exe
field:"InitiatingProcessFileName" kind:eq value:"services.exe"
InitiatingProcessFileNamein
  • services.exe
field:"InitiatingProcessFileName" kind:in value:"services.exe"
ProcessCommandLinene
  • msiexec.exe /V
field:"ProcessCommandLine" kind:ne value:"msiexec.exe /V"

Output Fields

These fields are emitted when the rule matches.

FieldSource
TimestampNetworkActproject-rename

Tailscale: Mass credential revocation in short window

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

Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise.

MITRE ATT&CK Coverage

TacticTechniques
Stealth

Rule Body

id: f817e2fa-6fa0-fc25-5369-cef9b58771af
name: "Tailscale: Mass credential revocation in short window"
description: |
  Identifies when five or more API keys, OAuth clients, or auth keys are revoked or deleted within one hour. May be routine rotation, or a typical cleanup pattern after credential compromise.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TailscaleCCF
    dataTypes:
      - Tailscale_Audit_CL
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - DefenseEvasion
  - Impact
relevantTechniques:
  - T1070
query: |
  Tailscale_Audit_CL
        | where Action in ("REVOKE", "DELETE")
        | where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT", "AUTH_KEY")
        | extend ActorLogin = tostring(Actor.loginName)
        | summarize
            RevokedCount = count(),
            TargetTypes = make_set(tostring(Target.type)),
            TargetIds = make_set(tostring(Target.id)),
            FirstEvent = min(TimeGenerated),
            LastEvent = max(TimeGenerated)
          by ActorLogin, bin(TimeGenerated, 1h)
        | where RevokedCount >= 5
        | project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: ActorLogin
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT5H
    matchingMethod: AllEntities
kind: Scheduled
version: 1.0.0

Stages and Predicates

Stage 1: source

Tailscale_Audit_CL

Stage 2: where

| where Action in ("REVOKE", "DELETE")

Stage 3: where

| where tostring(Target.type) in ("API_KEY", "OAUTH_CLIENT", "AUTH_KEY")

Stage 4: extend

| extend ActorLogin = tostring(Actor.loginName)

Stage 5: summarize

| summarize
          RevokedCount = count(),
          TargetTypes = make_set(tostring(Target.type)),
          TargetIds = make_set(tostring(Target.id)),
          FirstEvent = min(TimeGenerated),
          LastEvent = max(TimeGenerated)
        by ActorLogin, bin(TimeGenerated, 1h)

Stage 6: where

| where RevokedCount >= 5

Stage 7: project

| project TimeGenerated = LastEvent, ActorLogin, RevokedCount, TargetTypes, TargetIds, FirstEvent, LastEvent

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Actionin
  • DELETE
  • REVOKE
field:"Action" kind:in
RevokedCountge
  • 5 transforms: number
field:"RevokedCount" kind:ge value:"5"
Target.typein
  • API_KEY transforms: tostring, kusto_dynamic_path
  • AUTH_KEY transforms: tostring, kusto_dynamic_path
  • OAUTH_CLIENT transforms: tostring, kusto_dynamic_path
field:"Target.type" kind:in

Output Fields

These fields are emitted when the rule matches.

FieldSource
ActorLoginproject
FirstEventproject
LastEventproject
RevokedCountproject
TargetIdsproject
TargetTypesproject
TimeGeneratedproject

Trend Micro CAS - Ransomware infection

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

'Triggeres when ransomware was detected.'

MITRE ATT&CK Coverage

TacticTechniques
Impact

Rule Body

id: 0bec3f9a-dbe9-4b4c-9ff6-498d64bbef90
name: Trend Micro CAS - Ransomware infection
description: |
  'Triggeres when ransomware was detected.'
severity: High
status: Available
requiredDataConnectors:
  - connectorId: TrendMicroCAS
    dataTypes:
      - TrendMicroCAS
queryFrequency: 5m
queryPeriod: 5m
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Impact
relevantTechniques:
  - T1486
query: |
  TrendMicroCAS
  | where EventType =~ 'ransomware'
  | extend AccountCustomEntity = DstUserName, MalwareCustomEntity = RansomwareName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: AccountCustomEntity
  - entityType: Malware
    fieldMappings:
      - identifier: Name
        columnName: MalwareCustomEntity
version: 1.0.1
kind: Scheduled

Stages and Predicates

Stage 1: source

TrendMicroCAS

Stage 2: where

| where EventType =~ 'ransomware'

Stage 3: extend

| extend AccountCustomEntity = DstUserName, MalwareCustomEntity = RansomwareName

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
EventTypeeq
  • ransomware corpus 2 (kusto 2)
field:"EventType" kind:eq value:"ransomware"

Output Fields

These fields are emitted when the rule matches.

FieldSource
AccountCustomEntityextend
MalwareCustomEntityextend

Back to all Kusto rules