Asset-Abfragen¶
Abfragen für Azure Resource Graph und Log Analytics, um über einen verwalteten Bestand an virtuellen Maschinen zu berichten. Sie sind für die
Azure-Monitor-Datenquelle in Grafana geschrieben: Die Resource-Graph-Abfragen laufen im Modus Azure Resource Graph, die letzte läuft im
Modus Logs gegen den Log-Analytics-Workspace. Du kannst sie auch im Portal ausführen (Resource Graph Explorer, Log
Analytics), wenn du die Zeile entfernst, die die Dashboard-Variable ${vms} verwendet.
Die Strategie, die diese Abfragen unterstützen, steht unter Asset-Management, das Modul unter Managed-VM-Modul.
Gilt für das Managed-VM-Modul
Die Abfragen nutzen die Tags, die das Managed-VM-Modul auf die Resource Group einer Konfigurationsmenge schreibt. Wenn deine Plattform kein solches Modul hat, nutze sie als Muster und ersetze den Tag-Filter durch die Tags deines eigenen Tag-Sets (Namen und Tags). Teste sie zuerst auf einem kleinen Scope.
So erkennst du eine Managed VM¶
Davon hängt ab, ob man einem Bericht trauen kann, deshalb sei es ausdrücklich gesagt. Das Modul wendet seine Plattform-Tags auf die
Resource Group einer VM-Konfigurationsmenge an, ohne Bedingung: source, vm_configuration, environment, os_type,
vm_size, public_ip, shutdown_schedule_hours und ein Tag ownerN pro Konfigurations-Owner. Die Tags, die ein Kunde definiert,
landen stattdessen auf den VM-Ressourcen und sind optional.
Die verlässliche Definition lautet also:
Eine Managed VM ist eine virtuelle Maschine, deren Resource Group die Tags
sourceundvm_configurationträgt, die das Modul gesetzt hat.
Jede Abfrage unten beginnt mit diesem Filter und verknüpft dann mit den Ressourcen, deshalb enthalten sie einen resourcecontainers-
Join. Die Alternativen sind schlechter:
- Filtern nach dem VM-Namen. Das Modul benennt Maschinen
vm-<workload>-<configuration>-<env>-<location>-NN, aber nichts hindert ein Team daran, seine eigenen Maschinen genauso zu benennen, und die Legacy-Namensvariante (useOldVMNaming) folgt nicht dem Muster. - Filtern nach Tags der VM selbst. Das sind die Tags des Kunden, und sie können fehlen.
- Filtern nach der Maintenance Configuration. Sie lässt sich pro Konfigurationsmenge mit
disable_default_maintenance_configurationabschalten, sodass Maschinen fehlen würden. - Sich nur auf die Dashboard-Variable
${vms}verlassen. Sie ist ein Anzeigefilter, kein Scope-Filter. Wenn die Variable aus einer breiten Abfrage befüllt wird oder aufAllsteht, erscheinen nicht verwaltete Ressourcen im Ergebnis.
Die Variable ${vms}¶
Befülle die Dashboard-Variable aus derselben Definition, damit die Variable selbst immer nur Managed VMs anbietet:
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id)
| join kind=inner (
resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| project rgId, name
) on rgId
| distinct name
| order by name asc
In den Abfragen unten schränkt die Zeile mit ${vms} (zum Beispiel | where name in (${vms})) das Ergebnis auf die
Auswahl im Dashboard ein. Entferne diese Zeile, um über jede Managed VM zu berichten, unabhängig von der Auswahl.
Resource Graph unterstützt nur eine Teilmenge von KQL.
letundbag_keys()sind nicht verfügbar, und eine Abfrage darf höchstens dreijoin- und dreimv-expand-Operatoren enthalten. Die Abfragen unten bleiben innerhalb dieser Grenzen.
1. Inventar der Managed VMs¶
Die aktiven Managed VMs mit Konfiguration, Power State und Plattform-Tags.
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id),
vmConfiguration = tostring(tags['vm_configuration']),
environment = tostring(tags['environment']),
source = tostring(tags['source']),
owner1 = tostring(tags['owner1']),
owner2 = tostring(tags['owner2'])
| join kind=inner (
resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| extend
powerState = tostring(properties.extended.instanceView.powerState.code),
vmSize = tostring(properties.hardwareProfile.vmSize),
osType = tostring(properties.storageProfile.osDisk.osType),
imageOffer = tostring(properties.storageProfile.imageReference.offer),
imageSku = tostring(properties.storageProfile.imageReference.sku)
| project rgId, name, powerState, vmSize, osType, imageOffer, imageSku,
resourceGroup, location, subscriptionId
) on rgId
| where name in (${vms})
| project vmname = name, powerState, vmSize, osType, imageOffer, imageSku,
vmConfiguration, environment, source, owner1, owner2,
resourceGroup, location, subscriptionId
| order by vmname asc
2. Alle Assets der verwalteten Konfigurationsmengen¶
Jede Ressource, die zu einer verwalteten Konfigurationsmenge gehört, nicht nur die Maschinen: Disks, Netzwerkschnittstellen, Key Vault, Vaults, Data Collection Rules.
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id),
vmConfiguration = tostring(tags['vm_configuration']),
environment = tostring(tags['environment'])
| join kind=inner (
resources
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| project rgId, name, type, location, resourceGroup, subscriptionId
) on rgId
| project vmConfiguration, environment, resourceType = type, name,
resourceGroup, location, subscriptionId
| order by vmConfiguration asc, resourceType asc, name asc
Ergänze | summarize assetCount = count() by vmConfiguration, environment, resourceType für die
Anzahl pro Typ statt der einzelnen Ressourcen.
3. Managed VMs pro Owner¶
Das Modul schreibt pro Konfigurations-Owner ein Tag ownerN. bag_keys() ist in
Resource Graph nicht verfügbar, deshalb werden die Owner-Tags in ein Array gepackt und expandiert.
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| extend owners = pack_array(
tostring(tags['owner1']), tostring(tags['owner2']),
tostring(tags['owner3']), tostring(tags['owner4']))
| mv-expand owner = owners to typeof(string)
| where isnotempty(owner)
| project rgId = tolower(id), owner, vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| project rgId, name, location, subscriptionId
) on rgId
| project owner, vmname = name, vmConfiguration, location, subscriptionId
| order by owner asc, vmname asc
Erweitere das pack_array, falls eine Konfigurationsmenge jemals mehr als vier Owners hat.
4. Tag-Abdeckung¶
Resource Groups, die virtuelle Maschinen enthalten, denen aber ein oder mehrere Plattform-Tags fehlen. Eine Konfigurationsmenge, die über das Modul deployt wurde, trägt sie immer, daher ist jedes Ergebnis hier eine Resource Group, die außerhalb der pipeline erstellt wurde.
resources
| where type =~ 'microsoft.compute/virtualmachines'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| summarize vmCount = count() by rgId
| join kind=inner (
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| project rgId = tolower(id), resourceGroup = name, subscriptionId, tags
) on rgId
| extend
hasSource = isnotempty(tostring(tags['source'])),
hasVmConfiguration = isnotempty(tostring(tags['vm_configuration'])),
hasEnvironment = isnotempty(tostring(tags['environment'])),
hasOsType = isnotempty(tostring(tags['os_type'])),
hasVmSize = isnotempty(tostring(tags['vm_size'])),
hasOwner = isnotempty(tostring(tags['owner1']))
| where not(hasSource and hasVmConfiguration and hasEnvironment and hasOsType and hasVmSize and hasOwner)
| project resourceGroup, subscriptionId, vmCount,
hasSource, hasVmConfiguration, hasEnvironment, hasOsType, hasVmSize, hasOwner
| order by resourceGroup asc
5. Disks der Managed VMs¶
Speicher als Teil des Asset-Eintrags: Größe, SKU und Verschlüsselung.
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
resources
| where type =~ 'microsoft.compute/disks'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| extend
diskSizeGb = toint(properties.diskSizeGB),
diskState = tostring(properties.diskState),
skuName = tostring(sku.name),
encryptionType = tostring(properties.encryption.type),
attachedTo = tostring(split(tostring(properties.managedBy), '/', 8)[0])
| project rgId, diskName = name, diskSizeGb, skuName, diskState,
encryptionType, attachedTo, resourceGroup
) on rgId
| where isnotempty(attachedTo)
| where attachedTo in (${vms})
| project vmname = attachedTo, diskName, diskSizeGb, skuName, diskState,
encryptionType, vmConfiguration, resourceGroup
| order by vmname asc, diskName asc
6. Backup-Schutzstatus¶
Beide Vaults werden in die Resource Group der Konfigurationsmenge deployt, daher gilt derselbe Tag- Filter. Der Recovery Services Vault ist der Standard und der Backup Vault ist das Opt-in, deshalb werden beide kombiniert.
recoveryservicesresources
| where type =~ 'microsoft.recoveryservices/vaults/backupfabrics/protectioncontainers/protecteditems'
| extend
vmname = tostring(properties.friendlyName),
protectionState = tostring(properties.protectionState),
lastBackupStatus = tostring(properties.lastBackupStatus),
lastBackupTime = todatetime(properties.lastBackupTime),
policyName = tostring(properties.policyName),
vault = tostring(split(id, '/', 8)[0])
| project vmname, vault, policyName, protectionState, lastBackupStatus, lastBackupTime,
subscriptionId, resourceGroup
| union (
recoveryservicesresources
| where type =~ 'microsoft.dataprotection/backupvaults/backupinstances'
| extend
vmname = tostring(properties.friendlyName),
protectionState = tostring(properties.currentProtectionState),
lastBackupStatus = tostring(properties.protectionStatus.status),
lastBackupTime = todatetime(properties.latestRecoveryPointTime),
policyName = tostring(properties.policyInfo.name),
vault = tostring(split(id, '/', 8)[0])
| project vmname, vault, policyName, protectionState, lastBackupStatus, lastBackupTime,
subscriptionId, resourceGroup
)
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| project vmname, vmConfiguration, vault, policyName, protectionState,
lastBackupStatus, lastBackupTime
| order by vmname asc
7. Ausstehende Patches¶
Das Patch-Dashboard zeigt die letzte Patch-Installation. Diese Abfrage zeigt, was noch aussteht, also die andere Hälfte des Bildes.
patchassessmentresources
| where type =~ 'microsoft.compute/virtualmachines/patchassessmentresults'
| extend
vmname = tostring(split(id, '/', 8)[0]),
assessmentTime = todatetime(properties.startDateTime),
osType = tostring(properties.osType),
rebootPending = tostring(properties.rebootPending),
criticalAndSecurityPatchCount = toint(properties.criticalAndSecurityPatchCount),
otherPatchCount = toint(properties.otherPatchCount)
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| where vmname in (${vms})
| summarize arg_max(assessmentTime, *) by vmname
| project vmname, vmConfiguration, assessmentTime, osType, rebootPending,
criticalAndSecurityPatchCount, otherPatchCount
| order by criticalAndSecurityPatchCount desc
8. Netzwerk-Exposition¶
Keine Managed VM hat eine öffentliche IP. Diese Abfrage liefert die Managed VMs, die doch eine haben, das Ergebnis sollte also leer sein.
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
| join kind=inner (
resources
| where type =~ 'microsoft.network/networkinterfaces'
| extend rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| mv-expand ipConfig = properties.ipConfigurations
| extend
publicIpId = tostring(ipConfig.properties.publicIPAddress.id),
attachedTo = tostring(split(tostring(properties.virtualMachine.id), '/', 8)[0])
| where isnotempty(publicIpId) and isnotempty(attachedTo)
| project rgId, nic = name, publicIpId, attachedTo, resourceGroup, subscriptionId
) on rgId
| project vmname = attachedTo, vmConfiguration, nic, publicIpId, resourceGroup, subscriptionId
9. Letzte Änderungen an den Managed VMs¶
Resource Graph hält die Änderungshistorie vor. Ohne den Tag-Filter liefert die Abfrage Änderungen im gesamten Tenant, der Join beschränkt sie also auf den verwalteten Bestand.
resourcechanges
| extend
changeTime = todatetime(properties.changeAttributes.timestamp),
changeType = tostring(properties.changeType),
targetId = tostring(properties.targetResourceId),
targetType = tostring(properties.targetResourceType),
changedBy = tostring(properties.changeAttributes.changedBy)
| where changeTime > ago(30d)
| where targetType =~ 'microsoft.compute/virtualmachines'
| extend
resourceName = tostring(split(targetId, '/', 8)[0]),
rgId = tolower(strcat('/subscriptions/', subscriptionId, '/resourcegroups/', resourceGroup))
| join kind=inner (
resourcecontainers
| where type =~ 'microsoft.resources/subscriptions/resourcegroups'
| where isnotempty(tags['source']) and isnotempty(tags['vm_configuration'])
| project rgId = tolower(id), vmConfiguration = tostring(tags['vm_configuration'])
) on rgId
| project changeTime, vmname = resourceName, vmConfiguration, changeType, changedBy, targetId
| order by changeTime desc
Ändere targetType, um über einen anderen Ressourcentyp zu berichten, oder streiche diese Zeile, um jede Änderung
innerhalb der verwalteten Konfigurationsmengen zu sehen.
10. Software-Inventar innerhalb der Maschine¶
Change Tracking & Inventory schreibt in den Log-Analytics-Workspace. Diese Abfrage läuft auf der Azure-Monitor-Datenquelle im Modus Logs, nicht in Resource Graph.
Das Modul deployt pro VM-Konfigurationsmenge einen Workspace, der Workspace-Scope ist also schon der
verwaltete Bestand und ein Tag-Filter ist nicht nötig. let ist hier verfügbar, anders als in Resource Graph.
ConfigurationData
| where ConfigDataType == "Software"
| summarize arg_max(TimeGenerated, *) by Computer, SoftwareName, CurrentVersion
| project Computer, SoftwareName, CurrentVersion, Publisher, SoftwareType, TimeGenerated
| order by Computer asc, SoftwareName asc
Ersetze Software durch WindowsServices, Daemons, Files oder Registry für die anderen
Inventartypen. ConfigurationChange enthält dieselben Daten als Änderungshistorie statt als
aktuellen Zustand.