Azure Security Telemetry Toolkit

Practical KQL, Bicep, policy guidance, and a scorecard for the six-part Azure Security Telemetry series.

This toolkit supports the six-part Azure Security Telemetry series. It is deliberately practical: use it to start a review, validate assumptions, and adapt the patterns to your own subscriptions, workspaces, Sentinel deployment, Defender XDR portal, and Defender for Cloud configuration.

It is not a Microsoft standard. The scorecard included here is Chris Hailes’s practitioner framework for measuring whether Azure security telemetry is useful during real investigations, not merely collected.

Series articles

Prerequisites and boundaries

  • Azure Activity Log routed to a Log Analytics workspace where the AzureActivity table is available.
  • Resource diagnostic settings deployed to the services that matter for your investigation scenarios.
  • Microsoft Sentinel enabled where ASIM parsers and normalized hunting content are used.
  • Microsoft Defender XDR advanced hunting access in the Defender portal for identity, endpoint, cloud app, and alert-evidence queries.
  • Defender for Cloud enabled with continuous export configured to Log Analytics where SecurityAlert and SecurityRecommendation are queried.
  • Tenant-specific validation before production use. Category groups, diagnostic categories, table availability, and Defender XDR schema coverage vary by service, license, connector, and portal integration state.

Control-plane reconstruction KQL

Use this in Azure Monitor Logs or Microsoft Sentinel against the AzureActivity table. It reconstructs security-relevant management operations with caller, correlation, operation, target, result, and timing context.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
let lookback = 14d;
let securityRelevantOperations = dynamic([
  "Microsoft.Authorization/roleAssignments/write",
  "Microsoft.Authorization/roleAssignments/delete",
  "Microsoft.Authorization/policyAssignments/write",
  "Microsoft.Authorization/policyExemptions/write",
  "Microsoft.Insights/diagnosticSettings/write",
  "Microsoft.Insights/diagnosticSettings/delete",
  "Microsoft.Network/networkSecurityGroups/securityRules/write",
  "Microsoft.Network/networkSecurityGroups/write",
  "Microsoft.Network/azureFirewalls/write",
  "Microsoft.KeyVault/vaults/write",
  "Microsoft.Storage/storageAccounts/write",
  "Microsoft.Security/pricings/write"
]);
AzureActivity
| where TimeGenerated >= ago(lookback)
| where CategoryValue in ("Administrative", "Policy", "Security")
| where OperationNameValue in~ (securityRelevantOperations)
    or OperationNameValue has_any ("diagnosticSettings", "roleAssignments", "policyAssignments", "networkSecurityGroups", "keyVault", "storageAccounts")
| extend AuthorizationAction = tostring(Authorization_d.action),
         AuthorizationScope = tostring(Authorization_d.scope),
         RequestMethod = tostring(HTTPRequest.method),
         ClientRequestId = tostring(HTTPRequest.clientRequestId),
         ClaimsAppId = tostring(Claims_d.appid),
         ClaimsUpn = tostring(Claims_d.upn)
| project TimeGenerated,
          Caller,
          ClaimsUpn,
          ClaimsAppId,
          CallerIpAddress,
          CorrelationId,
          OperationId,
          OperationNameValue,
          ActivityStatusValue,
          ActivitySubstatusValue,
          SubscriptionId,
          ResourceGroup,
          ResourceProviderValue,
          ResourceId,
          AuthorizationAction,
          AuthorizationScope,
          RequestMethod,
          ClientRequestId,
          Properties_d
| order by TimeGenerated desc

Assumptions: AzureActivity contains subscription-level or management group level events that have been routed to the workspace. CorrelationId groups events from the same broader action, while OperationId identifies the operation event. Use Authorization_d, Claims_d, and Properties_d where available rather than parsing legacy string fields.

Tiered diagnostic-settings Bicep pattern

This sample shows a resource-class pattern, not a universal diagnostic policy. It uses Microsoft.Insights/diagnosticSettings@2021-05-01-preview, the Audit category group for mandatory evidence, and allLogs for a conditional investigative tier when a service and cost model justify it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@description('Existing Key Vault name used as the example high-risk resource class.')
param keyVaultName string

@description('Central Log Analytics workspace resource ID.')
param logAnalyticsWorkspaceId string

@allowed([
  'mandatoryEvidence'
  'investigative'
])
@description('mandatoryEvidence sends the Audit category group. investigative sends allLogs for deeper troubleshooting where justified.')
param telemetryTier string = 'mandatoryEvidence'

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: keyVaultName
}

resource diagnosticSetting 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
  name: 'set-by-platform-security-baseline'
  scope: keyVault
  properties: {
    workspaceId: logAnalyticsWorkspaceId
    logAnalyticsDestinationType: 'Dedicated'
    logs: telemetryTier == 'mandatoryEvidence' ? [
      {
        categoryGroup: 'Audit'
        enabled: true
      }
    ] : [
      {
        categoryGroup: 'allLogs'
        enabled: true
      }
    ]
    metrics: [
      {
        category: 'AllMetrics'
        enabled: false
      }
    ]
  }
}

Adaptation guidance: do not copy this blindly across resource providers. First list the diagnostic categories and category groups supported by the target resource type, then decide which categories are mandatory evidence and which are conditional investigation support. Use Azure Policy DeployIfNotExists for scale and drift control, but keep per-resource-class decisions visible in the baseline.

ASIM-aligned Sentinel normalization example

This query uses the Microsoft Sentinel ASIM authentication filtering parser. It is intended for workspaces where authentication sources are onboarded through supported ASIM parsers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
let lookback = 1d;
let failureThreshold = 10;
imAuthentication(starttime = ago(lookback), endtime = now())
| where EventType == "Logon"
| where EventResult in ("Failure", "Success")
| summarize
    FailedLogons = countif(EventResult == "Failure"),
    SuccessfulLogons = countif(EventResult == "Success"),
    FirstSeen = min(EventStartTime),
    LastSeen = max(EventEndTime),
    SourceIps = make_set(SrcIpAddr, 10),
    Products = make_set(EventProduct, 10)
    by TargetUsername, TargetUserId, LogonMethod
| where FailedLogons >= failureThreshold and SuccessfulLogons > 0
| project TargetUsername, TargetUserId, LogonMethod, FailedLogons, SuccessfulLogons, FirstSeen, LastSeen, SourceIps, Products
| order by FailedLogons desc

Assumptions: the parser returns ASIM fields such as EventType, EventResult, EventStartTime, EventEndTime, TargetUsername, TargetUserId, LogonMethod, SrcIpAddr, and EventProduct. Validate parser output with known events before promoting the query to an analytic rule.

Defender XDR advanced hunting examples

These examples run in Microsoft Defender XDR advanced hunting, not Azure Monitor Logs. Use them to add identity, device, cloud app, and alert-evidence context around Azure investigations. Do not assume direct joins to AzureActivity inside Defender XDR unless Microsoft Sentinel tables are onboarded into the Defender portal and visible to your role.

Identity and cloud-app context for privileged activity

1
2
3
4
5
6
7
let lookback = 1d;
let adminIndicators = dynamic(["Add member to role", "Add app role assignment", "Consent to application", "Update application"]);
CloudAppEvents
| where Timestamp >= ago(lookback)
| where IsAdminOperation == true or ActionType has_any (adminIndicators) or ActivityType has_any (adminIndicators)
| project Timestamp, AccountObjectId, AccountId, AccountDisplayName, Application, ActionType, ActivityType, IPAddress, CountryCode, OSPlatform, ObjectName, ObjectType, RawEventData
| order by Timestamp desc

Endpoint process context around a suspicious administrator

1
2
3
4
5
6
7
let lookback = 1d;
let accountUpn = "admin@contoso.com";
DeviceEvents
| where Timestamp >= ago(lookback)
| where InitiatingProcessAccountUpn =~ accountUpn
| project Timestamp, DeviceName, ActionType, InitiatingProcessFileName, InitiatingProcessCommandLine, InitiatingProcessSHA1, RemoteIP, RemoteUrl, ReportId
| order by Timestamp desc

Alert evidence for Azure or cloud-resource entities

1
2
3
4
5
6
let lookback = 7d;
AlertEvidence
| where Timestamp >= ago(lookback)
| where CloudPlatform =~ "Azure" or isnotempty(ResourceID) or EntityType in~ ("CloudResource", "AzureResource", "User", "Device")
| project Timestamp, AlertId, Title, Severity, ServiceSource, EntityType, EvidenceRole, AccountUpn, DeviceName, CloudResource, ResourceType, ResourceID, SubscriptionId
| order by Timestamp desc

Defender for Cloud continuous export

For scale, prefer the Microsoft built-in Azure Policy definitions for continuous export:

  • ffb6f416-7bd2-4488-8828-56585fef2be9 deploys export to a Log Analytics workspace for Defender for Cloud alerts and recommendations.
  • cdfcce10-4578-4ecd-9703-530938e4abcb deploys export to Event Hubs for Defender for Cloud alerts and recommendations.

Where you manage the automation resource directly, this Bicep pattern exports alerts, recommendations, recommendation snapshots, secure score, and attack paths to a Log Analytics workspace. Validate event-source selection and filters in a non-production subscription before broad assignment.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
param location string = resourceGroup().location
param workspaceResourceId string

resource continuousExport 'Microsoft.Security/automations@2019-01-01-preview' = {
  name: 'export-defender-for-cloud-to-law'
  location: location
  properties: {
    description: 'Continuous export of Defender for Cloud operational security findings to Log Analytics.'
    isEnabled: true
    scopes: [
      {
        description: 'Subscription scope'
        scopePath: subscription().id
      }
    ]
    sources: [
      {
        eventSource: 'Alerts'
        ruleSets: [
          {
            rules: [
              {
                propertyJPath: 'Severity'
                propertyType: 'String'
                expectedValue: 'high'
                operator: 'Equals'
              }
            ]
          }
          {
            rules: [
              {
                propertyJPath: 'Severity'
                propertyType: 'String'
                expectedValue: 'medium'
                operator: 'Equals'
              }
            ]
          }
        ]
      }
      {
        eventSource: 'Assessments'
      }
      {
        eventSource: 'AssessmentsSnapshot'
      }
      {
        eventSource: 'SecureScores'
      }
      {
        eventSource: 'SecureScoreControls'
      }
      {
        eventSource: 'AttackPaths'
      }
    ]
    actions: [
      {
        actionType: 'Workspace'
        workspaceResourceId: workspaceResourceId
      }
    ]
  }
}

Query exported recommendations and alerts from Log Analytics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let lookback = 30d;
SecurityRecommendation
| where TimeGenerated >= ago(lookback)
| summarize
    LatestTime = max(TimeGenerated),
    States = make_set(RecommendationState, 5),
    Severities = make_set(RecommendationSeverity, 5),
    SnapshotRecords = countif(IsSnapshot == true)
    by AssessedResourceId, RecommendationDisplayName, RecommendationId
| order by LatestTime desc
1
2
3
4
5
6
let lookback = 14d;
SecurityAlert
| where TimeGenerated >= ago(lookback)
| where ProductName has "Defender for Cloud" or ProviderName has "Security Center"
| project TimeGenerated, AlertName, AlertSeverity, Status, ProductName, ProviderName, ResourceId, CompromisedEntity, Tactics, Techniques, SystemAlertId, AlertLink
| order by TimeGenerated desc

Hailes Telemetry Utility Score

The Hailes Telemetry Utility Score is a reusable practitioner scorecard for Azure security telemetry. It measures whether telemetry reduces uncertainty during investigation, not whether a team has collected the most data.

Score each dimension from 0 to 5:

DimensionWhat good looks like
CompletenessCritical subscriptions, identities, resources, Defender findings, and response paths are represented.
FreshnessData arrives fast enough for the response decision it supports.
AccuracyFields are mapped correctly, parsers are tested, and source semantics are understood.
Accessibility and coverageThe right teams can query the right workspaces, tables, and portals without over-privilege.
Cost efficiencyIngestion and retention spend are tied to detection, investigation, assurance, or response value.
Tamper resilienceDiagnostic settings, workspaces, exports, retention, and administrative paths are protected and monitored.

Formula:

1
Hailes Telemetry Utility Score = ((completeness + freshness + accuracy + accessibility + costEfficiency + tamperResilience) / 30) * 100

Thresholds:

ScoreInterpretation
85-100Operationally dependable for the assessed scenario.
70-84Useful, but one or two dimensions can still break an investigation.
50-69Partial visibility; suitable for backlog prioritization, not assurance.
Below 50Collection exists, but the scenario is not defensible.

Example: a production privileged-access scenario scores completeness 4, freshness 4, accuracy 3, accessibility 4, cost efficiency 3, and tamper resilience 2. The score is 66.7. That does not mean the telemetry program failed; it means the next investment should be tamper resilience and parser validation, not another dashboard.

Limitations

  • KQL examples are starting points. Test against your own tables, connector versions, table retention, parser coverage, and role permissions.
  • AzureActivity, SecurityAlert, and SecurityRecommendation schemas are documented by Microsoft, but individual fields can be empty depending on provider, connector, export path, and event type.
  • Diagnostic category groups are not universal. Validate supported categories per resource type before assigning policy or Bicep modules at scale.
  • Defender XDR advanced hunting tables depend on deployed Defender services, licensing, data availability, portal integration, and role access.
  • Continuous export requires Defender for Cloud enablement, target write permissions, workspace solution requirements, and tenant-aware design for cross-tenant destinations.