In this module
1.4 Type Conversion and Coercion
Figure 1.4 — Change management pipeline for production automation.
Type Conversion and Coercion
Introduction
Figure — Type Conversion and Coercion. Applied to security investigation workflows at NE.
When data types do not match — a string that contains a number, a dynamic field that needs to be a string for comparison, a datetime that needs to be an integer for arithmetic — you need explicit type conversion. KQL provides conversion functions for every type. Using them correctly prevents the silent failures covered in subsection 1.3.
The conversion functions
| Function | Converts to | Example | Security use case |
|---|---|---|---|
tostring() | string | tostring(LocationDetails.city) | Extract string from dynamic JSON |
toint() | int (32-bit) | toint(ResultType) | Convert string ResultType for numeric comparison |
tolong() | long (64-bit) | tolong(Properties.targetResources[0].id) | Convert large numeric IDs |
toreal() | real (float) | toreal(LocationDetails.geoCoordinates.latitude) | Extract coordinates for distance calculation |
todatetime() | datetime | todatetime(Properties.createdDateTime) | Convert string dates from audit logs |
tobool() | bool | tobool(DeviceDetail.isManaged) | Convert string "true"/"false" to boolean |
totimespan() | timespan | totimespan("1:30:00") | Create duration values for arithmetic |
todecimal() | decimal | todecimal(Amount) | High-precision numeric conversion |
parse_json() | dynamic | parse_json(AdditionalDetails) | Parse string containing JSON into queryable object |
todynamic() | dynamic | todynamic(RawData) | Alias for parse_json |
Conversion behavior on failure
Every to* function returns null when conversion fails — not an error. This is both useful and dangerous:
// toint on a non-numeric string returns null, not an error
print toint("not_a_number") // output: null
// toint on a numeric string works
print toint("50126") // output: 50126
// todatetime on garbage returns null
print todatetime("not_a_date") // output: null// If ResultType contains "0", "50126", and "Interrupted"...
SigninLogs
| extend ResultCode = toint(ResultType)
| where ResultCode == 0
// "Interrupted" rows convert to null, then fail the == 0 check
// They are silently excluded — you never see them// WRONG: comparing dynamic to string silently fails in some contexts
SigninLogs
| where LocationDetails.countryOrRegion == "GB"
// CORRECT: explicit conversion
SigninLogs
| where tostring(LocationDetails.countryOrRegion) == "GB"AuditLogs
| extend CreatedDate = todatetime(
TargetResources[0].modifiedProperties[0].newValue)
| where CreatedDate > ago(7d)// Calculate sign-in duration in minutes
SigninLogs
| extend DurationMinutes = datetime_diff('minute',
todatetime(AppliedConditionalAccessPolicies[0].enforcedGrantControls),
TimeGenerated)// When a column contains JSON as a string, not as a dynamic type
| extend ParsedData = parse_json(RawEventData)
| extend ActionName = tostring(ParsedData.ActionName)
| extend TargetUser = tostring(ParsedData.Target[0].Id)SigninLogs
| extend Country = coalesce(
tostring(LocationDetails.countryOrRegion),
"Unknown"
)// Some sign-in events store the IP in different fields
SigninLogs
| extend BestIP = coalesce(IPAddress,
tostring(DeviceDetail.ipAddress),
"No IP recorded")SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType == "0"
| extend AuthDetails = parse_json(tostring(AuthenticationDetails))
| extend
PrimaryMethod = tostring(AuthDetails[0].authenticationMethod),
PrimaryResult = tostring(AuthDetails[0].succeeded),
SecondaryMethod = tostring(AuthDetails[1].authenticationMethod),
SecondaryResult = tostring(AuthDetails[1].succeeded)
| where PrimaryMethod != "" and SecondaryMethod != ""
| summarize count() by PrimaryMethod, SecondaryMethodSigninLogs
| where TimeGenerated > ago(24h)
| where not(ipv4_is_in_range(IPAddress, "10.0.0.0/8"))
and not(ipv4_is_in_range(IPAddress, "172.16.0.0/12"))
and not(ipv4_is_in_range(IPAddress, "192.168.0.0/16"))
| where ResultType == "0"| extend IPNum = tolong(split(IPAddress, ".")[0]) * 16777216
+ tolong(split(IPAddress, ".")[1]) * 65536
+ tolong(split(IPAddress, ".")[2]) * 256
+ tolong(split(IPAddress, ".")[3])
| sort by IPNum ascSigninLogs
| where TimeGenerated > ago(24h)
| extend ConvertedResult = toint(ResultType)
| summarize
TotalRows = count(),
ConvertedOK = countif(isnotnull(ConvertedResult)),
ConvertedNull = countif(isnull(ConvertedResult))
| extend FailRate = round(100.0 * ConvertedNull / TotalRows, 2)SigninLogs
| where TimeGenerated > ago(1h)
| where ResultType == "0"
| mv-expand CAPolicy = parse_json(tostring(ConditionalAccessPolicies))
| extend
PolicyName = tostring(CAPolicy.displayName),
PolicyResult = tostring(CAPolicy.result),
GrantControls = tostring(CAPolicy.enforcedGrantControls)
| where PolicyResult == "success"
| summarize AppliedPolicies = make_set(PolicyName) by UserPrincipalNameSigninLogs
| where TimeGenerated > ago(7d)
| where UserPrincipalName =~ "target@northgateeng.com"
| extend LocalHour = hourofday(TimeGenerated + 1h) // UTC+1 for BST
| summarize count() by LocalHour
| sort by LocalHour asc// An employee in New York (UTC-4) sends a suspicious email at 9am local = 13:00 UTC
// A recipient in London (UTC+1) clicks the link at 2pm local = 13:00 UTC
// Both events have the same UTC timestamp — the click was instant.
// Without UTC awareness, you might think the click happened 5 hours after the email.SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != "0"
| extend FailureReason = case(
ResultType == "50126", "Invalid password",
ResultType == "50074", "MFA required but not completed",
ResultType == "53003", "Blocked by Conditional Access",
ResultType == "50053", "Account locked out",
ResultType == "50057", "Account disabled",
ResultType == "50058", "Session interrupted",
ResultType == "50076", "MFA required — user did not complete",
ResultType == "500121", "Authentication failed during MFA",
ResultType == "700016", "Application not found in tenant",
strcat("Error code: ", ResultType) // default: show raw code
)
| summarize count() by FailureReason
| sort by count_ descSigninLogs
| where TimeGenerated > ago(24h)
| take 100
| extend ResultCode = toint(ResultType)
| extend ConversionWorked = isnotnull(ResultCode)
| summarize
Total = count(),
Converted = countif(ConversionWorked),
Failed = countif(not(ConversionWorked))Try it yourself
Run this query and examine the results:
Run this query and examine the results:
If Failed is greater than zero, some ResultType values in your data are not numeric. Check what they are: | where not(ConversionWorked) | distinct ResultType. These are the values a toint conversion would silently turn to null.
Check your understanding
1. You convert a column with toint() and use it in a where filter. The query returns fewer rows than expected. What is the most likely cause?
NE environmental considerations
NE's detection environment includes specific factors that influence this rule's operation:
Device diversity: 768 P2 corporate workstations with full Defender for Endpoint telemetry, 58 P1 manufacturing workstations with basic cloud-delivered protection, and 3 RHEL rendering servers with Syslog-only coverage. Rules targeting DeviceProcessEvents operate with full fidelity on P2 devices but may have reduced visibility on P1 devices. Manufacturing workstations in Sheffield and Sunderland represent a detection gap for endpoint-level detections.
The myth: Reading the Microsoft KQL documentation is sufficient to learn this concept. The documentation explains the syntax — what more do you need?
The reality: Documentation teaches SYNTAX. This course teaches APPLICATION. The KQL docs show how summarize works with generic examples. This course shows how summarize reveals a password spray pattern hiding in 45,000 sign-in events. The difference between knowing the syntax and knowing WHEN and WHY to use it in a security context is the difference between an analyst who copies queries and one who writes them. Every operator in this course is taught through a security investigation scenario — not abstract data manipulation.
Troubleshooting
"The query returns an error I do not understand." KQL error messages reference the specific line and operator that failed. Read the error message from left to right: it names the operator, the expected input type, and the actual input type. Most errors are type mismatches (passing a string where a datetime is expected) or field name typos. The getschema operator shows every field name and type for any table: TableName | getschema.
"The query runs but returns unexpected results." Add | take 10 after each operator in the pipeline and examine the intermediate output. This reveals WHERE the data transforms in a way you did not expect. Debug the pipeline stage by stage, not the entire query at once.
You are writing a KQL query and cannot remember the exact operator syntax. Do you check the Microsoft documentation or ask Claude?
Check the documentation first for SYNTAX questions (exact parameter names, return types, operator behavior). The KQL documentation at learn.microsoft.com/kusto is the authoritative source. Claude may generate plausible but incorrect syntax — especially for newer operators or recently changed behavior. Use Claude for DESIGN questions: 'I need to detect accounts with sign-ins from more than 3 countries in 24 hours — what is the best KQL approach?' Claude excels at designing the query strategy; the documentation excels at providing the exact syntax.
You've learned how KQL processes data.
K0 gave you the query language's place in the Microsoft security stack. K1 took you through the semantics — tables, operators, the pipe model, and why KQL isn't SQL. Now you write the queries that find what attackers hope you miss.
- 12 modules of query craft — filtering and shaping, joins and unions, time-series analysis, summarisation, string manipulation, and geospatial analysis
- 68 KQL exercises — every one with a realistic dataset, a reference solution, and a discussion of alternative approaches
- K11 — Threat Hunting with KQL — the course's flagship module. Hypothesis-driven methodology, MITRE ATT&CK-aligned hunting across 7 techniques, UEBA composite risk scoring, and retroactive IOC sweeps
- K13 Capstone — The Hunting Lab — three complete investigation scenarios requiring every query skill from the course
- Hunt management and ROI metrics — the operating model that justifies KQL hunt programs to leadership