AzureTracks

Practical Azure and Microsoft 365 security walkthroughs

· · · · · · ·

Saturday Heat – Bonus Walkthrough!

KQL from Raw Logs to Useful Security Detections

Kusto Query Language—better known as KQL—is one of the most useful skills available to Azure administrators, security engineers, and SOC analysts.

KQL allows us to explore large volumes of telemetry, isolate important events, identify patterns, summarize activity, build visualizations, and turn successful hunting queries into operational detections.

It is used throughout Azure Monitor, Log Analytics, Microsoft Sentinel, Microsoft Defender XDR, Azure Data Explorer, and several other Microsoft services.

The difficult part is rarely understanding what KQL can do. The challenge is knowing where to begin when you are staring at an unfamiliar table containing thousands—or millions—of records.

In this Saturday bonus article, we will build several practical security queries progressively. We will begin by reviewing raw Microsoft Entra sign-in data, add filters, summarize the results, extract values from dynamic fields, visualize activity, establish a simple behavioral baseline, correlate failures with successful sign-ins, and finish by turning a validated query into a Microsoft Sentinel analytics rule.

You do not need to memorize every KQL operator.

The most important skill is learning how to build, inspect, and validate a query one step at a time.

Let’s dive right in together!

Goal: Use KQL to move from raw Microsoft Entra sign-in logs to useful investigation results and a practical Microsoft Sentinel detection.

What are we solving?

Security platforms collect a tremendous amount of information, but collected information does not automatically become useful information.

A Log Analytics workspace may contain:

  • Microsoft Entra sign-ins
  • Azure Activity logs
  • Endpoint process events
  • Firewall and network events
  • Microsoft Defender alerts
  • Identity telemetry
  • Application logs
  • Cloud workload events
  • Threat intelligence
  • Microsoft Sentinel incidents

Without useful queries, high-value security signals can remain buried inside routine activity.

KQL gives us a repeatable way to answer operational questions such as:

  • Which users are generating repeated sign-in failures?
  • Which IP addresses are associated with those failures?
  • Did a successful sign-in occur after several failures?
  • Are successful sign-ins appearing from countries that are new for a user?
  • Which Azure resources were deleted during the last 24 hours?
  • Which devices executed suspicious PowerShell command lines?
  • Is an event reliable enough to become a Microsoft Sentinel detection?

KQL queries follow a pipeline structure. A query normally begins with a table and passes the records through filtering, parsing, summarization, sorting, and output stages. Microsoft recommends filtering data early so later operations process only the records relevant to the investigation.

The safest approach is to start with a broad but controlled view, inspect the available data, and progressively narrow the results.

Security outcome

  • Analysts can locate useful security telemetry without exporting it to another platform.
  • Repeated sign-in failures can be grouped into investigation-ready results.
  • Dynamic fields such as sign-in location can be converted into searchable columns.
  • Query results can be visualized to identify trends and spikes.
  • Current behavior can be compared against a historical baseline.
  • Failed and successful sign-ins can be correlated to identify higher-risk sequences.
  • Validated hunting queries can be converted into Microsoft Sentinel analytics rules.
  • Query performance is improved by filtering time and unnecessary data early.
  • Detection logic can be documented, tuned, versioned, and handed over to normal SOC operations.

What you will Deploy & Configure

  • A repeatable process for examining unfamiliar Log Analytics tables.
  • A progressive KQL query using the SigninLogs table.
  • Filtering and column-selection logic using where and project.
  • Aggregation using summarize, count, min, max, and make_set.
  • Time-based grouping using bin.
  • Dynamic-value extraction using extend and tostring.
  • A timechart showing failed sign-in activity.
  • A behavioral query identifying successful sign-ins from new countries.
  • A correlation query identifying success after repeated failures.
  • A scheduled Microsoft Sentinel analytics rule.
  • Entity mapping for the affected account and source IP address.
  • A validation, troubleshooting, tuning, and rollback process.

Microsoft baseline: Build and test the KQL query in the Logs experience before creating an analytics rule. The final analytics query should return only the records that require investigation and must preserve the TimeGenerated column for scheduled-rule processing.

Special Highlight: A query returning results is not automatically a good detection. A useful detection must be understandable, repeatable, sufficiently uncommon, operationally owned, and mapped to a response process.

Prerequisites

  • A Log Analytics workspace containing relevant security telemetry.
  • Microsoft Entra sign-in logs being sent to the workspace.
  • Microsoft Sentinel connected to the workspace if you plan to create an analytics rule.
  • Recent sign-in activity in the tenant.
  • Log Analytics Reader, Microsoft Sentinel Reader, or equivalent permissions for query testing.
  • Microsoft Sentinel Contributor or equivalent write permissions to create an analytics rule.
  • A test user, pilot group, or documented event that can be used during validation.
  • An identified SOC or platform owner for any resulting alert.
  • A defined review period before the rule is promoted into wider production use.

The SigninLogs table includes fields such as TimeGenerated, UserPrincipalName, IPAddress, AppDisplayName, ResultType, ConditionalAccessStatus, and dynamic sign-in details. Available fields and data can vary according to the sign-in type, tenant configuration, licensing, and diagnostic settings.

A query cannot return data from a table that is not being populated, regardless of how accurate the KQL syntax may be.

Before-state checklist

Capture the before state first. This creates documentation that administrators can use to understand what changed and provides evidence for a change request or Change Approval Board review.

  • Confirm the target Log Analytics workspace.
  • Record the Microsoft Sentinel workspace and subscription.
  • Confirm which Microsoft Entra diagnostic settings are enabled.
  • Verify that SigninLogs contains recent records.
  • Record the most recent TimeGenerated value.
  • Capture the current number of active analytics rules.
  • Review existing rules for similar sign-in failure detections.
  • Identify trusted VPN, proxy, security-service, and corporate egress IP addresses.
  • Identify expected service accounts and emergency-access accounts.
  • Record the initial detection threshold.
  • Define who will review resulting alerts.
  • Record the intended query frequency and lookback period.
  • Define the expected pilot duration.
  • Document how the rule will be disabled if it generates excessive noise.

A useful initial validation query is:

SigninLogs
| summarize
    RecordCount = count(),
    OldestRecord = min(TimeGenerated),
    NewestRecord = max(TimeGenerated)

The expected result is a record count greater than zero and a recent value in NewestRecord.

Lab build

Use a test workspace or a controlled production pilot first.

The following walkthrough uses the SigninLogs table, but the same progressive method can be used with other Microsoft Sentinel and Azure Monitor tables.

Open the Microsoft Defender portal and navigate to:

Microsoft Sentinel → Investigation & response → Hunting → Advanced hunting

Depending on the workspace experience, you may also use:

Microsoft Sentinel → General → Logs

or:

Azure portal → Log Analytics workspaces → Your workspace → Logs

Microsoft Sentinel is moving toward the Microsoft Defender portal as its primary operational experience. Microsoft currently documents that Azure portal support for Microsoft Sentinel will end after March 31, 2027, so new operational procedures should increasingly account for the Defender portal experience.

Step 1 – Confirm that the table contains data

Begin with the smallest useful query:

SigninLogs
| take 10

This returns up to ten records from SigninLogs.

The query is not yet useful as a detection, but it answers the first important question:

Does the table contain data that we can query?

Expand several records and review the available fields.

Useful columns may include:

  • TimeGenerated
  • UserPrincipalName
  • IPAddress
  • AppDisplayName
  • ResourceDisplayName
  • ResultType
  • ResultDescription
  • ConditionalAccessStatus
  • AuthenticationRequirement
  • LocationDetails
  • DeviceDetail
  • RiskLevelDuringSignIn
  • RiskState

When learning a new table, the available values are often more important than the column names alone.

A field named ResultType, for example, tells us little until we inspect the values it contains.

Step 2 – Add a time filter and select useful columns

Security queries should normally restrict the time range as early as possible.

SigninLogs
| where TimeGenerated >= ago(24h)
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    AppDisplayName,
    ResourceDisplayName,
    ResultType,
    ResultDescription,
    ConditionalAccessStatus
| order by TimeGenerated desc
| take 50

This query:

  1. Reads records from SigninLogs.
  2. Limits the data to the last 24 hours.
  3. Returns only the columns required for the investigation.
  4. Places the newest events first.
  5. Limits the displayed results to 50 rows.

The pipe character passes the output from one operation into the next.

I like to think of each pipe as another refinement stage:

Get data
    |
Filter time
    |
Select fields
    |
Sort results
    |
Return final records

Microsoft’s KQL guidance recommends applying selective filters early because this reduces the amount of data processed by later operations.

Step 3 – Filter for failed sign-ins

Successful Microsoft Entra sign-ins normally have a ResultType value of "0".

We can exclude those records to focus on failures:

SigninLogs
| where TimeGenerated >= ago(24h)
| where ResultType != "0"
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    AppDisplayName,
    ResourceDisplayName,
    ResultType,
    ResultDescription,
    ConditionalAccessStatus
| order by TimeGenerated desc

This query is already more useful.

Instead of reviewing every sign-in, we are reviewing only unsuccessful activity.

There may still be hundreds of separate failure records, however. We need to group related events into something an analyst can review efficiently.

Step 4 – Summarize repeated failures

The summarize operator aggregates multiple records into a smaller result set.

SigninLogs
| where TimeGenerated >= ago(24h)
| where ResultType != "0"
| summarize
    FailedAttempts = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    Applications = make_set(AppDisplayName, 10),
    FailureReasons = make_set(ResultDescription, 10)
    by UserPrincipalName, IPAddress
| order by FailedAttempts desc

This groups failed sign-ins by user and source IP address.

Instead of displaying every individual failure, it provides:

  • The number of failed attempts
  • The first observed failure
  • The most recent failure
  • The applications involved
  • The reported failure reasons

The summarize operator is one of the most important tools in KQL because it converts high-volume event data into aggregated investigation results.

Step 5 – Add a threshold and time buckets

A user mistyping a password once is rarely interesting.

Ten failures during a short period may deserve attention.

let FailureThreshold = 10;
SigninLogs
| where TimeGenerated >= ago(24h)
| where ResultType != "0"
| summarize
    FailedAttempts = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    Applications = make_set(AppDisplayName, 10),
    FailureReasons = make_set(ResultDescription, 10)
    by
        UserPrincipalName,
        IPAddress,
        WindowStart = bin(TimeGenerated, 15m)
| where FailedAttempts >= FailureThreshold
| order by FailedAttempts desc

There are two important additions.

First, the let statement creates a reusable threshold:

let FailureThreshold = 10;

This makes the query easier to read and tune.

Second, the bin function groups events into 15-minute periods:

WindowStart = bin(TimeGenerated, 15m)

Without time buckets, ten failures spread throughout an entire day could be grouped together and appear more suspicious than they are.

Thresholds must be tuned against the normal behavior of your environment. Ten failures may be meaningful in one tenant and routine in another.

Step 6 – Extract country and city from a dynamic field

Some KQL columns contain structured information stored as a dynamic object.

LocationDetails is a common example.

We can extract individual properties and convert them into normal columns:

SigninLogs
| where TimeGenerated >= ago(24h)
| extend
    Country = tostring(LocationDetails.countryOrRegion),
    State = tostring(LocationDetails.state),
    City = tostring(LocationDetails.city)
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    AppDisplayName,
    ResultType,
    Country,
    State,
    City
| order by TimeGenerated desc

The extend operator creates calculated columns while preserving the original record.

The tostring() function converts the selected dynamic value into a searchable string.

We can now summarize successful sign-ins by country:

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType == "0"
| extend Country = tostring(LocationDetails.countryOrRegion)
| where isnotempty(Country)
| summarize
    SuccessfulSignins = count(),
    UniqueUsers = dcount(UserPrincipalName)
    by Country
| order by SuccessfulSignins desc

This provides a useful tenant-level view of sign-in geography.

Remember that IP-derived location is contextual information—not definitive proof of a user’s physical location. VPN services, mobile carriers, secure web gateways, cloud proxies, and other network architectures can affect geolocation.

Step 7 – Visualize sign-in failures over time

KQL can render query results directly as a chart.

SigninLogs
| where TimeGenerated >= ago(7d)
| where ResultType != "0"
| summarize
    FailedSignins = count()
    by bin(TimeGenerated, 1h)
| render timechart

This shows hourly failed sign-in volume during the last seven days.

Charts are useful for identifying:

  • Sudden authentication spikes
  • Repeating activity at specific times
  • Changes following a policy deployment
  • Differences between weekday and weekend activity
  • Thresholds that may create excessive alert volume

The render operator is useful during investigation, hunting, workbooks, and query tuning. Remove it before converting the query into an analytics rule because analytics rules require tabular output rather than a visualization.

Step 8 – Find successful sign-ins from a country that is new for the user

We can compare recent activity against a longer historical period.

The following query establishes a baseline using successful sign-ins from the previous 14 days, excluding the most recent day.

It then identifies successful user-and-country combinations that appear during the last day but did not appear in the baseline.

let BaselinePeriod = 14d;
let RecentPeriod = 1d;
let KnownUserCountries =
    SigninLogs
    | where TimeGenerated between (ago(BaselinePeriod) .. ago(RecentPeriod))
    | where ResultType == "0"
    | extend Country = tostring(LocationDetails.countryOrRegion)
    | where isnotempty(UserPrincipalName)
    | where isnotempty(Country)
    | summarize by UserPrincipalName, Country;
SigninLogs
| where TimeGenerated >= ago(RecentPeriod)
| where ResultType == "0"
| extend
    Country = tostring(LocationDetails.countryOrRegion),
    State = tostring(LocationDetails.state),
    City = tostring(LocationDetails.city)
| where isnotempty(UserPrincipalName)
| where isnotempty(Country)
| join kind=leftanti KnownUserCountries on UserPrincipalName, Country
| project
    TimeGenerated,
    UserPrincipalName,
    IPAddress,
    AppDisplayName,
    ResourceDisplayName,
    Country,
    State,
    City,
    ConditionalAccessStatus,
    RiskLevelDuringSignIn
| order by TimeGenerated desc

The important operation is:

| join kind=leftanti KnownUserCountries on UserPrincipalName, Country

A leftanti join returns records from the recent dataset that do not have a matching user-and-country combination in the historical dataset.

This is a useful hunting lead, but it is not proof of malicious behavior.

Expected reasons for new countries can include:

  • User travel
  • Corporate VPN changes
  • Mobile carrier routing
  • Security proxy routing
  • New remote-work locations
  • Changes to Microsoft or third-party service infrastructure
  • Incomplete historical data

This query should initially be used as a hunting query or workbook result. Promote it into an analytics rule only after expected locations and network paths have been reviewed.

Step 9 – Correlate repeated failures with a later successful sign-in

A successful sign-in following repeated failures from the same user and IP address may be more important than failures alone.

The following query correlates the two event types:

let FailureThreshold = 5;
let SearchPeriod = 1h;
let FailedSignins =
    SigninLogs
    | where TimeGenerated >= ago(SearchPeriod)
    | where ResultType != "0"
    | summarize
        FailedAttempts = count(),
        FirstFailure = min(TimeGenerated),
        LastFailure = max(TimeGenerated),
        FailureReasons = make_set(ResultDescription, 10)
        by UserPrincipalName, IPAddress
    | where FailedAttempts >= FailureThreshold;
let SuccessfulSignins =
    SigninLogs
    | where TimeGenerated >= ago(SearchPeriod)
    | where ResultType == "0"
    | summarize
        SuccessTime = max(TimeGenerated),
        Applications = make_set(AppDisplayName, 10),
        Resources = make_set(ResourceDisplayName, 10)
        by UserPrincipalName, IPAddress;
FailedSignins
| join kind=inner SuccessfulSignins on UserPrincipalName, IPAddress
| where SuccessTime between (LastFailure .. LastFailure + 30m)
| project
    TimeGenerated = SuccessTime,
    UserPrincipalName,
    IPAddress,
    FailedAttempts,
    FirstFailure,
    LastFailure,
    SuccessTime,
    Applications,
    Resources,
    FailureReasons
| order by SuccessTime desc

This query:

  1. Finds users and IP addresses with at least five failures.
  2. Finds successful sign-ins from the same user and IP address.
  3. Joins the two result sets.
  4. Keeps successes occurring within 30 minutes after the final failure.
  5. Preserves a TimeGenerated column for possible analytics-rule use.

This is still not automatic proof of compromise.

A legitimate user may fail authentication several times before successfully entering the correct password. The value comes from combining the query with additional context such as:

  • User risk
  • Sign-in risk
  • New country or network
  • Unmanaged device
  • Unfamiliar application
  • Impossible or abnormal travel
  • Conditional Access results
  • Privileged account status
  • Subsequent mailbox, endpoint, or Azure activity

Step 10 – Prepare the failed-sign-in query for an analytics rule

The following version is designed as a clean starting point for a scheduled Microsoft Sentinel rule:

let FailureThreshold = 10;
SigninLogs
| where TimeGenerated >= ago(30m)
| where ResultType != "0"
| summarize
    FailedAttempts = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    Applications = make_set(AppDisplayName, 10),
    FailureReasons = make_set(ResultDescription, 10)
    by
        UserPrincipalName,
        IPAddress,
        WindowStart = bin(TimeGenerated, 15m)
| where FailedAttempts >= FailureThreshold
| project
    TimeGenerated = WindowStart,
    UserPrincipalName,
    IPAddress,
    FailedAttempts,
    FirstSeen,
    LastSeen,
    Applications,
    FailureReasons

The final project operation creates the TimeGenerated field required by the scheduled analytics-rule workflow.

Before creating the rule, run the query against several different periods:

  • Last 24 hours
  • Last 7 days
  • Last 14 days

Record:

  • Number of results
  • Accounts involved
  • IP addresses involved
  • Failure reasons
  • Expected service-account activity
  • Trusted corporate or VPN sources
  • Whether results are actionable

Step 11 – Create the Microsoft Sentinel scheduled analytics rule

In the Microsoft Defender portal, navigate to:

Microsoft Sentinel → Configuration → Analytics

Then:

  1. Select Create.
  2. Select Scheduled query rule.
  3. Enter a descriptive rule name.

Example:

Repeated Microsoft Entra Sign-in Failures by User and IP
  1. Add a clear description.

Example:

Identifies a user and source IP address generating at least ten failed Microsoft Entra sign-ins within a fifteen-minute time bucket.
  1. Set the initial severity to Medium.
  2. Select the applicable MITRE ATT&CK tactic and technique based on your finalized detection logic.
  3. Keep the rule disabled during initial configuration if you want to complete validation before execution.
  4. Paste the tested KQL into the rule query field.
  5. Confirm that the wizard validates the query.
  6. Map the account entity:
Entity: Account
Identifier: FullName
Value: UserPrincipalName
  1. Map the IP entity:
Entity: IP
Identifier: Address
Value: IPAddress
  1. Add useful custom details:
FailedAttempts
FirstSeen
LastSeen
Applications
FailureReasons
  1. Configure the initial schedule:
Run query every: 15 minutes
Lookup data from the last: 30 minutes
  1. Configure the alert threshold to generate an alert when the query returns one or more results.
  2. Configure incident creation.
  3. Group alerts using the account and IP address where appropriate.
  4. Avoid attaching automatic containment during the initial pilot.
  5. Review the configuration.
  6. Create the rule.
  7. Enable it only when the pilot owner and SOC reviewer are ready.

Microsoft’s analytics-rule workflow supports entity mapping, custom details, configurable query intervals, lookback periods, incident creation, and alert grouping. The query interval can range from five minutes to 14 days, and the lookback period must be at least as long as the interval.

Bonus KQL examples

The progressive method used above can be applied to many other security scenarios.

Azure resource deletions

This query identifies successful Azure resource deletion operations during the last 24 hours:

AzureActivity
| where TimeGenerated >= ago(24h)
| where ActivityStatusValue == "Success"
| where OperationNameValue endswith "/delete"
| project
    TimeGenerated,
    Caller,
    CallerIpAddress,
    SubscriptionId,
    ResourceGroup,
    ResourceId,
    OperationNameValue,
    CorrelationId
| order by TimeGenerated desc

The AzureActivity table is populated when Azure Activity Log data is sent to the Log Analytics workspace. Activity Log records describe subscription-level and resource-provider control-plane operations.

Conditional Access failures

This query summarizes sign-ins that failed Conditional Access evaluation:

SigninLogs
| where TimeGenerated >= ago(24h)
| where ConditionalAccessStatus =~ "failure"
| summarize
    FailedSignins = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    SourceIPs = make_set(IPAddress, 20),
    Applications = make_set(AppDisplayName, 20),
    ResultDescriptions = make_set(ResultDescription, 20)
    by UserPrincipalName
| order by FailedSignins desc

This is useful when reviewing:

  • Policy rollout effects
  • Legacy authentication
  • Device-compliance failures
  • Location-based access controls
  • Authentication-strength requirements
  • Unexpected application access

A Conditional Access failure is not necessarily malicious. Review which policy applied and whether the failure represents a working security control or an unexpected access problem.

Suspicious PowerShell command lines

The following query is intended for Microsoft Defender XDR Advanced Hunting or an environment where DeviceProcessEvents is available:

DeviceProcessEvents
| where Timestamp >= ago(24h)
| where FileName in~ ("powershell.exe", "pwsh.exe")
| where ProcessCommandLine has_any (
    "-enc",
    "-encodedcommand",
    "DownloadString",
    "Invoke-Expression",
    "IEX"
)
| project
    Timestamp,
    DeviceName,
    AccountUpn,
    FileName,
    ProcessCommandLine,
    InitiatingProcessFileName,
    InitiatingProcessCommandLine
| order by Timestamp desc

DeviceProcessEvents contains process-creation and related endpoint events when Microsoft Defender for Endpoint telemetry is available.

The strings in this sample can also appear in legitimate administrative scripts. Review the initiating process, signer, user, device role, network activity, and surrounding timeline before escalating.

Daily alert volume by severity

SecurityAlert
| where TimeGenerated >= ago(7d)
| summarize
    AlertCount = count()
    by AlertSeverity, bin(TimeGenerated, 1d)
| order by TimeGenerated asc
| render timechart

This is a simple way to review whether alert volume is increasing and whether severity distribution is useful.

If nearly everything is marked High severity, the severity model may not be helping analysts prioritize work.

Change-window notes

  • Build the query before opening the production change.
  • Run the query over historical data to estimate alert volume.
  • Record the expected number of daily results.
  • Identify accounts or IP ranges requiring documented exclusions.
  • Keep the first detection pilot intentionally narrow.
  • Create the rule disabled when approval requires a separate activation step.
  • Schedule activation when an analyst can immediately review results.
  • Do not attach automatic containment during the first pilot.
  • Preserve screenshots of the query results and final rule settings.
  • Record query text, threshold, schedule, lookback, entities, and incident settings in the change ticket.
  • Pre-stage the rule-disable procedure as the primary rollback.
  • Do not delete the query during rollback; preserve it for analysis and tuning.

A smaller but fully validated detection is more valuable than a large rule deployment with unclear ownership and excessive alerts.

Production hardening

For production, treat KQL detections as managed security content—not as one-time queries stored in an analyst’s browser history.

  • Use meaningful names for rules, queries, workbooks, and functions.
  • Add comments to explain thresholds and unusual query logic.
  • Use let statements for thresholds and reusable values.
  • Apply the time filter near the beginning of the query.
  • Select only required columns.
  • Avoid expensive joins against unnecessary data.
  • Limit dynamic expansion to the properties required.
  • Preserve TimeGenerated in analytics-rule output.
  • Map relevant account, host, IP, cloud-resource, URL, and file entities.
  • Add useful fields as custom alert details.
  • Separate hunting queries from production detections.
  • Review false positives before adding exclusions.
  • Make exclusions specific rather than global.
  • Store rules and queries in source control.
  • Record the owner, version, approval, and last-tuned date.
  • Review detection performance and query execution regularly.
  • Use rule insights and execution-management features when available to identify failures or inefficient rules.
  • Export production rules to infrastructure-as-code or another controlled configuration format.
  • Test material changes in a sandbox workspace.
  • Use automation only after the detection has demonstrated stable results.
  • Review Microsoft-authored analytics-rule templates before creating duplicate custom logic.

For high-volume data, consider whether a summary rule, transformation, or more efficient table strategy is appropriate. Microsoft Sentinel summary rules can aggregate verbose data into smaller custom tables for reporting and analysis scenarios.

Operational Handoff

Here is a short list of what is needed to hand the detection over to normal SOC operations:

  • Store the final KQL with the Sentinel rule documentation.
  • Record the purpose of the detection.
  • Document the data-source and connector dependency.
  • Record the threshold and why it was chosen.
  • Document entity mappings.
  • Record expected false-positive scenarios.
  • Identify trusted IP ranges and service accounts.
  • Document the analyst investigation steps.
  • Identify the escalation owner.
  • Define the response time.
  • Record the rule schedule and lookback period.
  • Define the tuning-review cadence.
  • Record the rollback method.
  • Add the rule to the monthly analytics-rule review.
  • Review exclusions quarterly.
  • Retire rules that no longer provide useful security value.
  • Revalidate the rule whenever its source table, connector, or schema changes.

A simple analyst runbook for the repeated-failure rule could be:

  1. Confirm the affected user and source IP.
  2. Review the complete sign-in timeline.
  3. Check whether a successful sign-in followed the failures.
  4. Review country, city, application, device, and Conditional Access results.
  5. Check user and sign-in risk.
  6. Confirm whether the user was active at the time.
  7. Review whether the IP belongs to a trusted VPN or proxy.
  8. Review endpoint and cloud activity following the sign-in.
  9. Reset credentials or revoke sessions only when the investigation supports containment.
  10. Document the reason for closure or escalation.

Finished-state Validation

ControlExpected resultEvidence to capture
Data ingestionSigninLogs contains recent recordsTable query and newest timestamp
Raw-data queryTen sample records return successfullyQuery screenshot
Failure filteringSuccessful records are excludedFiltered results
AggregationRepeated failures are grouped by user and IPSummarized result set
Time bucketsResults are separated into 15-minute windowsQuery output
Dynamic parsingCountry, state, and city appear as columnsParsed location results
VisualizationFailed sign-ins appear on a timechartChart screenshot
Historical baselineNew user-country combinations are returnedBaseline query results
CorrelationFailure-then-success sequences can be identifiedCorrelation results
Detection queryFinal output contains TimeGeneratedQuery schema
Rule validationAnalytics-rule wizard reports successful validationRule review page
Entity mappingUser and IP entities appear in generated alertsAlert entity page
Incident creationTest activity creates the expected incidentSentinel incident
Alert ownershipA named team or analyst receives the incidentSOC runbook
RollbackRule can be disabled without deleting evidenceChange record
GovernanceQuery and configuration are stored in source controlRepository or content package

Cost and Licensing Notes

Running KQL queries does not normally create a separate per-query charge in the same way that deploying a new Azure workload would.

The major cost drivers are generally associated with:

  • Log ingestion
  • Data retention
  • Archive or restoration
  • Search jobs
  • Auxiliary or Basic log plans
  • Microsoft Sentinel enablement
  • Microsoft Defender licensing
  • Data duplication across workspaces
  • High-volume connectors
  • Workbook and rule designs that repeatedly scan unnecessary data

An inefficient query is primarily an operational and performance problem, but high-volume data collection and retention can create material cost.

Before onboarding an additional log source, confirm:

  • Expected daily ingestion volume
  • Table plan
  • Retention requirement
  • Detection value
  • Compliance requirement
  • Whether duplicate telemetry is already available
  • Whether all event categories are required
  • Whether ingestion-time transformation is appropriate

Azure Monitor supports different table types, plans, retention configurations, and transformation options. Query design should account for the table plan and the available query capabilities.

Known Limitations and Edge Cases

  • A query cannot compensate for missing telemetry.
  • SigninLogs does not include every identity event type.
  • Non-interactive, service-principal, and managed-identity sign-ins can appear in separate tables.
  • Field availability can vary by sign-in type.
  • IP geolocation is not definitive.
  • VPNs and security proxies can make a country appear new.
  • Baseline queries require enough historical data to be meaningful.
  • New users may naturally trigger first-seen detections.
  • A successful sign-in after failures can be legitimate.
  • Service accounts may generate repetitive patterns.
  • Dynamic fields can be empty or change structure.
  • Advanced Hunting and Log Analytics schemas are not identical.
  • Some Defender XDR tables use Timestamp rather than TimeGenerated.
  • A hunting query may require modification before becoming a Sentinel rule.
  • Query results can overlap when the lookback period is longer than the execution interval.
  • Alert grouping may be required to prevent duplicate incidents.
  • Suppression can hide legitimate repeated activity if configured too broadly.
  • Large joins and dynamic-field expansion can affect query performance.
  • Analytics rules require ongoing tuning as user behavior and infrastructure change.

Microsoft Entra also has separate tables for non-interactive users, service principals, and managed identities. Select the table that matches the identity activity being investigated rather than assuming that all authentication events are in SigninLogs.

Pro Tip: If you are running into either polar opposite here, too much / not enough data; go ahead and check your Microsoft Entra ID data connector and validate which tables are feeding your log analytics workspace. You will need elevated permissions such as Global Admin to make changes to this connector, and you should follow any required change processes you have in place > Especially if you are a doing a limited production PoC.

Troubleshooting

The table does not exist

Confirm that the correct data connector or diagnostic setting is enabled.

Also confirm that you are running the query in the correct workspace.

Check spelling and capitalization. KQL table names are case-sensitive.

The table exists but returns no records

Increase the selected time range and run:

SigninLogs
| summarize
    RecordCount = count(),
    NewestRecord = max(TimeGenerated)

If the count is zero, review the Microsoft Entra diagnostic setting and destination workspace.

The query reports an unknown column

Run:

SigninLogs
| take 1

Inspect the schema and confirm that the field exists in your environment.

Do not assume that every table or connector exposes identical fields.

A dynamic-field expression returns blank values

Inspect the original field:

SigninLogs
| where TimeGenerated >= ago(1h)
| project LocationDetails
| take 20

Confirm the property name and capitalization before extracting it.

The query is slow

  • Reduce the time range.
  • Place selective where filters earlier.
  • Remove unused columns.
  • Avoid expanding entire dynamic objects.
  • Reduce the size of joined datasets.
  • Summarize before joining when appropriate.
  • Test each query section separately.
  • Remove unnecessary sorting.
  • Avoid broad wildcard searches when a specific field is available.

The analytics rule rejects the query

Confirm that:

  • The query returns tabular results.
  • The query contains TimeGenerated.
  • There is no render statement.
  • All tables and columns exist in the rule’s workspace.
  • The query does not depend on an unsupported cross-workspace function.
  • Each let statement ends with a semicolon.
  • Entity-mapped fields exist in the final output.

The rule creates duplicate alerts

Review:

  • Query interval
  • Lookback period
  • Time buckets
  • Incident grouping
  • Alert grouping
  • Suppression settings

A 15-minute query interval with a 30-minute lookback intentionally overlaps data. This can accommodate ingestion delay, but the rule must be grouped or tuned to avoid unnecessary duplicate incidents.

The rule creates too many alerts

Do not immediately suppress the rule globally.

First:

  1. Review the most common users.
  2. Review the most common IP addresses.
  3. Review failure descriptions.
  4. Identify trusted network paths.
  5. Separate user and service-account behavior.
  6. Increase the threshold carefully.
  7. Narrow the targeted account or application scope.
  8. Add specific, documented exclusions.
  9. Rerun the historical query.
  10. Record the reason for every tuning change.

The rule creates no alerts

Confirm that:

  • The rule is enabled.
  • The query returns results during the same lookback period.
  • The alert threshold is correct.
  • The rule is using the expected workspace.
  • The source data is current.
  • The query frequency is correct.
  • There are no rule-execution errors.
  • The rule output contains TimeGenerated.

Rollback or cleanup

For a lab or test rule:

  • Disable the analytics rule.
  • Preserve the query text.
  • Export or capture the rule configuration.
  • Remove test incidents if permitted by the operating procedure.
  • Remove temporary watchlists or test exclusions.
  • Retain screenshots and validation evidence.
  • Document why the test was concluded.

For a production rule:

  • Disable the rule as the first rollback action.
  • Do not delete it during the initial rollback.
  • Preserve incidents and alerts already generated.
  • Record the rollback time and approver.
  • Restore the previous known-good query or threshold if appropriate.
  • Remove automation bindings if they contributed to the issue.
  • Confirm that no containment playbook remains active.
  • Review the before-state evidence.
  • Tune the rule in the test workspace.
  • Re-enable it only after validation and approval.

Queries themselves are normally non-disruptive.

The greater operational risk comes from:

  • Excessive alert volume
  • Incorrect incident creation
  • Broad suppression
  • Misleading entity mapping
  • Automatic containment
  • Undocumented exclusions
  • Poorly understood detection logic

Think of the query as the beginning of the control—not the end.

Final takeaways

  • KQL becomes easier when we stop trying to write the finished detection in a single attempt.
  • Start with the table.
  • Inspect a few records.
  • Add a time filter.
  • Select the fields that matter.
  • As recent attacks have taught us, do not exclude larger files
  • Filter the activity.
  • Summarize related events.
  • Add time buckets.
  • Extract useful context.
  • Compare current activity with historical behavior.
  • Correlate related event types.
  • Only then should the query become an alert.
  • A good KQL query does more than return interesting data.
  • A good production detection produces a small number of understandable, actionable results that an analyst knows how to investigate.

That is the real path from raw logs to security operations.

About AzureTracks

AzureTracks publishes practical Azure and Microsoft 365 security guidance with deployment, validation, operational, and rollback considerations.

This walkthrough is designed for cloud engineers, security administrators, and SOC teams who want to move beyond copying sample queries and begin building KQL detections that are understandable, testable, and ready for operational use.

About AzureTracks

AzureTracks publishes practical Azure and Microsoft 365 security guidance with deployment, validation, operational, and rollback considerations.

Learn more about Andrew and the site’s technical approach →