Tag Archive: KQL

DisruptionAndResponseEvents Table Preview

What can be more exciting than new Advance Hunting tables. Since it’s Preview it could change

The DisruptionAndResponseEvents table in Microsoft Defender XDR is your visibility layer into automatic attack disruption – Microsoft’s built-in capability to actively stop attacks in progress.

It records what Defender did, not just what the attacker did.

Telemetry

Most telemetry tells you what happened while this table tells you how an attack was disrupted in real time and it helps you understand attack scope, attack impact and Defender’s automated response decisions.

What kind of events you’ll see?

The table focuses on defensive actions

  • Containment actions
  • Examples:
  • User logon blocked
  • SMB access blocked
  • RPC access denied
  • RDP sessions disconnected

Hardening & protection policies

  • Safe Boot blocked
  • Safe Boot Guard applied
  • Group Policy hardening applied
  • SYSVOL access blocked

These indicate environment hardening triggered automatically

Session / activity termination

  • SMB session killed
  • Remote session logged off

These show active attacker sessions being cut off

Important nuance (most people miss this) the table does NOT log the action itself (like “Contain User”) instead, it logs the resulting effects, such as:

  • Blocked logons
  • Denied access
  • Policy enforcement

Data sources & dependencies

Populated from multiple Defender services (Endpoint, Identity, etc.) which requires proper onboarding – otherwise you’ll see little or no data

No onboarding = no visibility.

This table shines when you want to answer:

  • What did Defender automatically stop?
  • Why did this attack fail?
  • Which users/devices were actively contained?
  • What hardening actions were triggered during an incident?

If you want to have the table extended with the description you can use the following User-defined function (which runs in query time in Defender)

For further information about Attack Disruption, please visit: Automatic attack disruption in Microsoft Defender – Microsoft Defender XDR | Microsoft Learn

#Happy Hunting

Using Exposure Graph Data to get Domain Admins and Domain Controllers dynamically

When working with both Threat Hunting and Detection Engineering it has been a challenge to dynamically get domain administrators and domain controllers. Sometimes, a valid guess was the best approach.

With the Exposure Graph data, we can dynamically retrieve these entities.

Another thing which will simplify your queries is to save this as a function

Example code for Domain Admins

//Get Domain Admin accounts (member of "Domain Admins")
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project accountName = json.accountName,
                        Identifiers,
                        accountEnabled=json.accountEnabled,
                        distinguishedName=json.distinguishedName,
                        adminCount=json.adminCount,
                        AccountControl=json.userAccountControl,
                        passwordUpdateTime=json.passwordUpdateTime,
                        createdDateTime = json.createdDateTime,
                        sidHistory=json.sidHistory,
                        accountDomain=json.accountDomain,
                        nestedAdGroupNames = json.nestedAdGroupNames

Example code of usage

//One usage of DAs
let DAs = 
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| distinct tostring(json.accountName);
IdentityLogonEvents
| where AccountName has_any (DAs)

//Another example
let DAs = 
ExposureGraphNodes
| extend json = (parse_json(NodeProperties)).rawData
| where json.nestedAdGroupNames has "Domain Admins"
| where NodeLabel == "user"
| distinct tostring(json.accountName);
DeviceLogonEvents
| where AccountName has_any (DAs)
| summarize DistinctDevices = dcount(DeviceName), 
            NetworkSuccessLogons = countif(ActionType == "LogonSuccess" and LogonType == "Network"), 
            RemoteInteractiveSuccessLogons = countif(ActionType=="LogonSuccess" and LogonType  =="RemoteInteractive"),
            TotalRemoteDeviceNames = dcount(RemoteDeviceName),
            RemoteIPs = dcount(RemoteIP),
            FailedLogins =countif(ActionType == "LogonFailed"),
            AttemptedLogins = countif(ActionType=="LogonAttempted"), 
            ProtocolsUsed = dcount(Protocol)
         by AccountName

Domain controllers

ExposureGraphNodes
| extend _jsonBlob = (parse_json(NodeProperties)).rawData
| extend DeviceName = tolower((_jsonBlob.deviceName)),
         Roles = _jsonBlob.deviceRole,
         OSVersionBuild = strcat(tostring(_jsonBlob.osVersionFriendlyName),"-",tostring(_jsonBlob.osReleaseId),"-",tostring(_jsonBlob.osBuild)),
         OSVersion = tostring(_jsonBlob.osVersionFriendlyName),
         ExposureScore = tostring(_jsonBlob.exposureScore),
         SenseClientVersion = tostring(_jsonBlob.senseClientVersion),
         isHybridAzureADJoined = _jsonBlob.isHybridAzureADJoined,
         isAzureADJoined = _jsonBlob.isAzureADJoined,
         SensorHealthState = tostring(_jsonBlob.sensorHealthState),
         DeviceRiskScore = tostring(_jsonBlob.riskScore),
         isInternetFacing = _jsonBlob.isInternetFacing,
         RDPStatus = _jsonBlob.rdpStatus,
         RemoteServices = _jsonBlob.remoteServicesInfo,
         TPMInfo = _jsonBlob.tpmData,
         VulnHasHighOrCritical = _jsonBlob.highRiskVulnerabilityInsights.hasHighOrCritical,
         VulnMaxCvssScore = _jsonBlob.highRiskVulnerabilityInsights.maxCvssScore
| extend RDPServiceStatus = RDPStatus.serviceRunning,
         RDPAllowConnections = RDPStatus.allowConnections
| where Roles has "DomainController"          
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project-away NodeId,NodeLabel,NodeName,NodeProperties,_jsonBlob, Categories, Roles,RDPStatus

//Another example

let DCs = ExposureGraphNodes
| extend _jsonBlob = (parse_json(NodeProperties)).rawData
| extend DeviceName = tolower((_jsonBlob.deviceName)),
         Roles = _jsonBlob.deviceRole,
         OSVersionBuild = strcat(tostring(_jsonBlob.osVersionFriendlyName),"-",tostring(_jsonBlob.osReleaseId),"-",tostring(_jsonBlob.osBuild)),
         OSVersion = tostring(_jsonBlob.osVersionFriendlyName),
         ExposureScore = tostring(_jsonBlob.exposureScore),
         SenseClientVersion = tostring(_jsonBlob.senseClientVersion),
         isHybridAzureADJoined = _jsonBlob.isHybridAzureADJoined,
         isAzureADJoined = _jsonBlob.isAzureADJoined,
         SensorHealthState = tostring(_jsonBlob.sensorHealthState),
         DeviceRiskScore = tostring(_jsonBlob.riskScore),
         isInternetFacing = _jsonBlob.isInternetFacing,
         RDPStatus = _jsonBlob.rdpStatus,
         RemoteServices = _jsonBlob.remoteServicesInfo,
         TPMInfo = _jsonBlob.tpmData,
         VulnHasHighOrCritical = _jsonBlob.highRiskVulnerabilityInsights.hasHighOrCritical,
         VulnMaxCvssScore = _jsonBlob.highRiskVulnerabilityInsights.maxCvssScore
| extend RDPServiceStatus = RDPStatus.serviceRunning,
         RDPAllowConnections = RDPStatus.allowConnections
| where Roles has "DomainController"          
| mv-apply EntityIds on (
                            summarize Identifiers = make_bag(
                                                            pack(
                                                                    tostring(EntityIds.type), 
                                                                    EntityIds.id
                                                                )
                                                        )
                        )
| project-away NodeId,NodeLabel,NodeName,NodeProperties,_jsonBlob, Categories, Roles,RDPStatus
| distinct _DeviceName;
DeviceProcessEvents
| where DeviceName in(DCs)

Happy Hunting!

Quick tip – Country Codes

All countries has an ISO code, described in ISO 3166 is an international standard.
These codes are used throughout the IT industry by computer systems and software to make it easier to identify a country.

It has multiple formats and they country codes are presented in the following formats: Alpha-2 (2 characters), Alpha-3 (3 characters) and Numeric (3 digits).

In the data from some logs like SigninLogs and IdentityLogonEvents the country is presented as Alpha-2. We realized pretty quick is that some 2-characters country codes are difficult to remember. As in below, picture it could be difficult to know all these countries.

We have been using this for a long time and thought it might be something others can use as well.

So to solve this I created a csv file and placed on github:

https://raw.githubusercontent.com/mattiasborg82/Hunting/main/General/cc.txt

To be able to join our data with this file we can use the external data operator in Kusto

Since it’s a CSV file, we can make it more usable by split the rows on comma

To to build the full use-case for this, we join it with our SigninLogs (or other logs that uses the country code)

Copy friendly code

let CountryCodes = externaldata (CountryCode:string)
[
 @"https://raw.githubusercontent.com/mattiasborg82/Hunting/main/General/cc.txt"
]
with(ignoreFirstRecord=true);
SigninLogs
| where isnotempty(Location)
| join kind=leftouter (
    CountryCodes
    | extend Country = tostring(split(CountryCode, ",")[0]),
              Location = tostring(split(CountryCode, ",")[1])
    | project-away CountryCode)
on Location
| summarize count() by Country,UserDisplayName

This can be used further to combine with conditional access blocks showing potential credential leak

Happy Hunting!

Microsoft Sentinel Parsing tips – Whitespace control

This post will be a part of a multiple posts to cover data parsing in Microsoft Sentinel.

Intro

Kusto is a powerfull query language and easy to adopt.

Even if Kusto is very powerfull, working with custom log sources is, sometimes, a mess. Some parsers requires more effort and some are very simple.

In general, when it’s possible to use operators like “parse” (link) function or “parse-kv” (link) it’s very welcome. However, the reality has a different challenge for us.

In this post we want to share a quick pro tip to solve the mystic of hidden whitespaces

The challenge of whitespaces

Whitespaces ” ” exists everywhere, the challenge is how it’s presented in log analytics.

Log analytics does a lot for the user in terms of nicely present data. It actually removes duplicate whitespaces, as well as leading and trailing whitespace.

This could result in problems like failing parsers, regex and string operators like “==”, “startswith”, “endswith” etc will fail. Especially if it’s not consistent.

Marking the string in the output view does not show the extra whitespaces

Copying the text and paste into a text-editor will not show it either like in below example where we copied the output into VS Code (we can only see one dot to show one whitespace between foo and bar)

However, the double whitespaces are interpreted during execution, and it’s only in the presentation view the extra is removed. As in below example, we used split on ” ” to show the existence of the double whitespace.

When working with multiple log sources you don’t want to search and see if they exist (which may change during the log source life cycle), you rather want a way to always make the log to look good in your parser.

Solution

To properly address this (if there aren’t any good ways to change the audit settings of the system sending the logs)

To handle the duplicate white spaces we use the replace_regex function (link here) and use the whitespace “\s” with the quantifier “+” which means one or multiple times and replace it with a space ” “.

This will search for spaces (one or more) and replace it with a single, because we don’t want to remove single spaces. And by using the same column name “SyslogMessage” we will actually reuse the same column for our clean output.

Please note that this will not change the message in the database, only during execution.

Doing this gives us the following output.

The next step is that we want to remove the leading and trailing whitespaces. If we for instance expect the first character to be a value, the leading whitespace could make our parser to fail or an analytic rule.

We have seen occasions where this happens from time to time and not all messages in a log source.

To fix the leading and trailing whitespaces we use another regex to look for start of string and end of string. But this time we want to replace with “nothing/null” which is why we can’t use this regex in the first cleaning.

In the second run we use the same column name again to cleanup the SyslogMessage. There is a best practice to always keep the original message, however, this is to solve an error from the log source and not to alter the SyslogMessage.

The regex starts with an anchor “^” to define the start of the string and followed by a whitespace “\s” since we cleaned all double whitespaces we don’t need to use the quantifier. To handle the trailing whitespace we use the OR operand “|” and check for a whitespace “\s” followed by the anchor “$” to determine the end of the string. If we get any hits it will be replaced with null and we have a clean string.

By adding these 2 lines of code to the parser, we will avoid running into strange issues which could take some time to troubleshoot.

//Sample
CustomLogSource_CL
| extend SyslogMessage = replace_regex(SyslogMessage,@"\s+",@" ") //Remove duplicate whitespaces
| extend SyslogMessage = replace_regex(SyslogMessage,@"^\s|\s$",@"") //Remove leading and trailing whitespaces

Happy Hunting!