Cloud

Azure WAF: read Application Gateway blocks with KQL without chasing every layer

Build useful KQL queries to identify requests blocked by Azure Web Application Firewall on Application Gateway, with action, ruleId, URI, client IP, hostname and time window.

25 May 2026 azurewafapplication-gatewaykqllog-analyticssecurity

A WAF in prevention mode can block a legitimate request as well as a real attack. The hardest part is not always seeing that a block exists, but quickly understanding what was blocked, by which rule, from which source and on which endpoint. Without a method, analysis drifts: WAF, backend, DNS, certificates, while the logs already contain part of the answer.

The scenario here is an Azure Application Gateway with Web Application Firewall enabled and logs sent to Log Analytics. The goal is to build a few basic KQL queries to read blocks, group events and prepare a clean investigation before changing a rule.

Running use case: a partner portal blocked after release

The example followed in this article is a partner portal published on partner.example.com. The backend is healthy, Application Gateway probes are green, but one partner reports that the comment form has been failing since 09:15 with a WAF error page. The team has three clues: source IP 203.0.113.10, endpoint /partner/comment, and an incident window of about one hour.

The goal is not to create an exclusion yet. First prove that the block really comes from WAF, identify the rule, measure whether the issue affects one partner or the whole form, and produce evidence that application and security teams can review.

Start from the right table

Depending on diagnostic settings and collection mode, logs can appear in different tables. In many older environments, Application Gateway WAF events are in AzureDiagnostics with Category == "ApplicationGatewayFirewallLog". In newer configurations, resource-specific tables may exist. First confirm where events arrive.

kusto 01-find-waf-logs.kql
search "ApplicationGatewayFirewallLog"
| summarize count() by $table
| order by count_ desc

This query does not replace configuration knowledge, but it prevents searching in the wrong table. Once the table is confirmed, adapt queries to the schema actually available.

List recent blocks

The first useful query should stay simple: short period, WAF category, blocking action, actionable fields. The goal is to see recent events before deciding whether they are false positives.

kusto 02-recent-blocks.kql
AzureDiagnostics
| where TimeGenerated > ago(2h)
| where Category == "ApplicationGatewayFirewallLog"
| where action_s =~ "Blocked"
| project TimeGenerated,
        hostname_s,
        requestUri_s,
        clientIp_s,
        ruleSetType_s,
        ruleSetVersion_s,
        ruleId_s,
        message_s,
        details_message_s,
        policyId_s
| order by TimeGenerated desc

Exact column names can vary by table and configuration. If a column does not exist, inspect the schema and adapt. Keep time, URI, client IP, action, rule and message together.

Group by rule and URI

An isolated event is rarely enough. To prioritize, group events. One rule blocking a single suspicious request does not carry the same weight as a rule blocking every submission of a business form since deployment.

kusto 03-blocks-by-rule-uri.kql
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "ApplicationGatewayFirewallLog"
| where action_s =~ "Blocked"
| summarize blocks=count(),
          firstSeen=min(TimeGenerated),
          lastSeen=max(TimeGenerated),
          sampleMessage=any(message_s)
by hostname_s, requestUri_s, ruleId_s, ruleSetType_s, ruleSetVersion_s
| order by blocks desc

This view helps distinguish background noise from a real application issue. If one ruleId appears massively on one endpoint, inspect what that endpoint receives before creating an exception.

Correlate with an IP or incident window

When a user or partner reports a block, the investigation should start from a time window, source IP, hostname or path. The more targeted the query, the more useful the result.

kusto 04-incident-window.kql
let startTime = datetime(2026-05-25 09:00:00);
let endTime = datetime(2026-05-25 10:00:00);
let sourceIp = "203.0.113.10";
AzureDiagnostics
| where TimeGenerated between (startTime .. endTime)
| where Category == "ApplicationGatewayFirewallLog"
| where clientIp_s == sourceIp
| project TimeGenerated,
        action_s,
        hostname_s,
        requestUri_s,
        ruleId_s,
        message_s,
        details_message_s
| order by TimeGenerated asc

This gives a timeline. It shows whether the request was detected, blocked, repeated, or whether several rules triggered on the same journey.

Turn the result into operational evidence

For the partner portal, the expected output should be easy to paste into an incident ticket. A line such as 09:18, partner.example.com, /partner/comment, 203.0.113.10, Blocked, 942100, SQL Injection Attack Detected is more useful than a screenshot from the Azure portal. It lets the application team find the form, the security team understand the rule, and operations verify whether the issue repeated.

text incident-evidence.txt
WAF incident
Application: partner.example.com
Endpoint: /partner/comment
Source: 203.0.113.10
Window: 09:00 - 10:00
Action: Blocked
Rule: 942100
Volume: 18 blocks over 42 minutes
Hypothesis: free-text field triggering a SQLi rule

This evidence does not decide the fix yet. It creates a shared baseline for the next step: false positive, real attack, application correction or targeted exclusion.

Look at detected events before assuming blocks

In detection mode, logs do not mean the request was blocked. In prevention mode, some events may contribute to scoring or be logged depending on the ruleset and policy. Analysis must distinguish action from actual impact.

kusto 05-actions-summary.kql
AzureDiagnostics
| where TimeGenerated > ago(24h)
| where Category == "ApplicationGatewayFirewallLog"
| summarize events=count() by action_s, ruleId_s, message_s
| order by events desc

This avoids a classic mistake: treating every WAF event as a block. Before changing a rule, confirm that the action actually prevented the expected request.

Conclusion

Reading WAF blocks with KQL should start simple: find the right table, filter blocking actions, project useful fields, then group by rule, URI, source and period. The goal is not to create an exception quickly. The goal is to produce readable evidence.

A good WAF investigation answers four questions: which request was blocked, by which rule, on which endpoint and at what volume. Until those answers are clear, changing OWASP, CRS or DRS means treating a symptom without understanding the risk.