Skip to content

Azure

Section 8 · From docs/INVENTORY_INTERNALS.md on GitHub

8.1 Collection paths

AzureDeepInventoryCollector(credential, subscription_id, graph_client_factory=None, use_resource_graph=True):

  • Resource Graph (default, needs azure-mgmt-resourcegraph): collect_subscription_graph(client, subscription_id, *, role_assignments=True, defender=True, errors=None) runs four queries through run_query() (paging with skip tokens, throttling back-off): resources (every resource with full properties; must succeed), resourcecontainers (resource groups and the subscription), authorizationresources (role assignments, joined with role definitions for names), and securityresources (Defender for Cloud pricings). The last three are best-effort; failures land in errors and coverage. collection_mode becomes "resource-graph".
  • SDK fallback (Resource Graph unavailable or the resources query failed): the base AzureCollector per-service collectors, the network fabric fetchers (NICs, public IPs, load balancers, route tables, NAT gateways, private endpoints, application gateways, firewalls, disks, scale sets, disk encryption sets), an ARM resources.list() sweep (discovered_via: "arm-sweep", no properties), resource groups and the subscription, all fed through the same AzureAssetBuilder. collection_mode becomes "sdk".

8.2 AzureAssetBuilder and drafts

AzureAssetBuilder(subscription_id, discovered_via="resource-graph") turns ARM-shaped rows into assets:

  1. add_row(row) creates a _Draft: id, lower-cased ARM type, name, props (properties), asset_type (asset_type_from_arm: the catalog plus kind overrides such as function apps → CLOUD_FUNCTION), region, account_id, tags, and the base metadata (resource_type, kind, sku, resource_group, collected_via, zones, plan).
  2. _extract(draft) runs the common extractors for every row, then the extractors registered for the draft's type.
  3. build() resolves role assignments into GRANTS_ACCESS / POLICY_ALLOWS_ACTION relations on the principal (or an Entra placeholder), applies deferred relations, and emits assets (properties sanitised, relations de-duplicated, aliases unique).

_Draft API used by extractors:

Member Purpose
d.row, d.props, d.type, d.id, d.name, d.region, d.account_id, d.tags The row and its parsed fields.
d.md Metadata dictionary to fill.
d.add(*relations) Add relations (rel(...) results); None and self-references are ignored.
d.alias(*values) Add identifiers (host names, principal refs, workspace IDs).
d.exposed Set True for internet exposure.

Builder hooks: b.register_principal(principal_id, draft) (the draft owns this managed identity), b.note_principal(id, type), b.register_kubelet(object_id, draft) (AKS kubelet identity, used to derive AcrPull image pulls), b.defer(owner_id, relation) (add a relation to another draft after all rows are processed).

Common extractors (registry._COMMON): managed identity (system-assigned principal as alias, user-assigned identities as ASSUMES_ROLE / RUNS_ON), managedBy (MANAGES / OWNED_BY, or the attached VM for disks), parent containment (nested type → parent with VPC_CONTAINS_SUBNET / CLUSTER_CONTAINS_SERVICE, else resource group, else subscription), private endpoint connections, diagnostic workspace references (LOGS_TO). Helpers for extractors: _set_exposure (public network access without a deny default action), _network_rule_grants (VNet and resource instance rules → GRANTS_ACCESS with network_rule), _keyvault_key_ref (customer-managed keys → ENCRYPTED_BY_KMS to the vault host).

8.3 Registered ARM types

Extractors are registered with @extractor("microsoft.<ns>/<type>", ...) (lower case) in five modules:

Module Area ARM types
azure_graph/compute.py virtual machines, scale sets, disks, disk encryption sets, AKS clusters and agent pools microsoft.compute/diskencryptionsets, microsoft.compute/disks, microsoft.compute/virtualmachines, microsoft.compute/virtualmachinescalesets, microsoft.containerservice/managedclusters, microsoft.containerservice/managedclusters/agentpools
azure_graph/network.py VNets, subnets, NSGs, NICs, public IPs, route tables, NAT gateways, private endpoints and link services, connections, firewall policies, private DNS links, flow logs microsoft.network/connections, microsoft.network/firewallpolicies, microsoft.network/natgateways, microsoft.network/networkinterfaces, microsoft.network/networksecuritygroups, microsoft.network/networkwatchers/flowlogs, microsoft.network/privatednszones/virtualnetworklinks, microsoft.network/privateendpoints, microsoft.network/privatelinkservices, microsoft.network/publicipaddresses, microsoft.network/routetables, microsoft.network/virtualnetworks, microsoft.network/virtualnetworks/subnets
azure_graph/edge.py load balancers, application gateways, Azure Firewall, VNet gateways, Bastion, Front Door and CDN endpoints, origins, custom domains, security policies microsoft.cdn/profiles/afdendpoints, microsoft.cdn/profiles/customdomains, microsoft.cdn/profiles/endpoints, microsoft.cdn/profiles/origingroups/origins, microsoft.cdn/profiles/securitypolicies, microsoft.network/applicationgateways, microsoft.network/azurefirewalls, microsoft.network/bastionhosts, microsoft.network/frontdoors, microsoft.network/loadbalancers, microsoft.network/virtualnetworkgateways
azure_graph/data_security.py Key Vault, storage, SQL and open-source databases, Cosmos DB, messaging, Event Grid, cognitive services, Redis, search, App Configuration, ML, Synapse, Data Factory, Databricks, Log Analytics, Application Insights, managed identities, backup vaults microsoft.appconfiguration/configurationstores, microsoft.cache/redis, microsoft.cognitiveservices/accounts, microsoft.databricks/workspaces, microsoft.datafactory/factories, microsoft.dataprotection/backupvaults, microsoft.dbformariadb/servers, microsoft.dbformysql/flexibleservers, microsoft.dbformysql/servers, microsoft.dbforpostgresql/flexibleservers, microsoft.dbforpostgresql/servers, microsoft.documentdb/databaseaccounts, microsoft.eventgrid/domains, microsoft.eventgrid/systemtopics, microsoft.eventgrid/systemtopics/eventsubscriptions, microsoft.eventgrid/topics, microsoft.eventgrid/topics/eventsubscriptions, microsoft.eventhub/namespaces, microsoft.insights/components, microsoft.keyvault/vaults, microsoft.machinelearningservices/workspaces, microsoft.managedidentity/userassignedidentities, microsoft.operationalinsights/workspaces, microsoft.recoveryservices/vaults, microsoft.search/searchservices, microsoft.servicebus/namespaces, microsoft.signalrservice/signalr, microsoft.sql/servers, microsoft.sql/servers/databases, microsoft.storage/storageaccounts, microsoft.synapse/workspaces
azure_graph/web_containers.py ACR, App Service sites / slots / plans / environments, Container Apps and jobs, managed environments, container instances, API Management microsoft.apimanagement/service, microsoft.app/containerapps, microsoft.app/jobs, microsoft.app/managedenvironments, microsoft.containerinstance/containergroups, microsoft.containerregistry/registries, microsoft.web/hostingenvironments, microsoft.web/serverfarms, microsoft.web/sites, microsoft.web/sites/slots

71 ARM types have dedicated extractors; every other type still gets the common extractors and a catalog classification.

Role assignments: one GRANTS_ACCESS / POLICY_ALLOWS_ACTION relation per assignment, from the principal to the scope, with role, role_definition_id, privileged (Owner, Contributor, User Access Administrator, *Administrator, *Data Owner), principal_type, assignment_id and condition. Principals that are managed identities of collected resources resolve to those resources; others become entra:principal/<id> placeholders. An AKS kubelet identity holding AcrPull on a registry adds USES_IMAGE / RUNS_ON from the cluster to the registry.

Defender for Cloud: one THREAT_DETECTOR per plan, security_service: "defender-<plan>", enabled when the tier is Standard, MONITORS the subscription when enabled.

8.4 Depth beyond the breadth query

The resources query gives breadth. Depth comes from four more sources, each split into named tasks the way the AWS deep collector splits its work. Every task writes one ServiceCoverage record to AzureDeepInventoryCollector.coverage, and a failing task never stops the others.

flowchart LR
    ARG[Resource Graph: resources] --> B[AzureAssetBuilder]
    T[Resource Graph tables: policy, security, backup, ...] --> B
    R[ARM REST: child resources and subscription listings] --> B
    D[Data planes: AI Search, Synapse] --> B
    K[Kubernetes API of AKS clusters] --> B
    B --> A[CloudAssets with declared relations]
    G[Microsoft Graph: Entra ID, once per tenant] --> M[InventoryMapper]
    A --> M
    M --> L[RelationshipLinker]
Source Module Tasks
More Resource Graph tables azure_graph/tables.py management_group_role_assignments, role_definitions, policy, policy_states, backup_items, kubernetes_configuration, network_resources, guest_configuration, maintenance, insights, security_assessments, secure_score, security_alerts
ARM REST azure_graph/children.py deny_assignments, pim_eligibility, lighthouse, locks, deployments, deployment_stacks, activity_log, managed_identity_credentials, key_vault_items, apim, functions, web_apps, static_sites, service_bus, event_hubs, stream_analytics, data_factory, synapse, kusto, sql, cosmos, storage, dns_records, dns_resolver, virtual_wan, network_manager, front_door, acr, aks, cognitive_services, machine_learning, search, automation, app_configuration, monitor_private_link, batch, spring_apps, data_protection, dev_center, diagnostic_settings, sentinel
Data planes azure_graph/dataplane.py search_data_plane, synapse_data_plane
Kubernetes API inventory/azure_kubernetes.py aks_kubernetes, recorded per cluster as kubernetes:<cluster name>
Microsoft Graph inventory/azure_entra.py entra, recorded per listing as entra_users, entra_groups, ... (see 8.8)

The Resource Graph tasks run only on the Resource Graph path. On the SDK fallback they are recorded SKIPPED with "Resource Graph unavailable", and the REST, data-plane and Kubernetes tasks still run.

Coverage status of a task:

  • SUCCESS: every call answered. A 404, 400, 405 or 409 counts as "this parent has none of these" (for example gateways on a Consumption APIM), not as a failure.
  • PARTIAL: some calls were refused (403) or failed, the others returned data.
  • FAILED: every call failed, or the Resource Graph query raised.
  • SKIPPED: not selected by inventory.services, turned off in configuration, or the credential could not issue a token for the audience the task needs.

azure_full becomes PARTIAL when any task record is FAILED or PARTIAL, the same rule aws_full follows.

Service filtering

inventory.services and inventory.exclude_services (and --services / --exclude-services) apply to Azure as well. families.select_azure_tasks() mirrors aws_deep_tasks.select_tasks(): a name matches a task or its family, exclude wins, and kubernetes is a pseudo-family that only toggles in-cluster AKS mapping (selected with containers too). The families are the AWS names: compute, network, storage, data, analytics, ml, identity, containers, serverless, integration, security, logging, operations, dns, hybrid, iac, governance, backup, cicd, kubernetes, sweep.

resources and resource_containers always run. With a filter set, the breadth rows are kept only for the selected families (family_of_type() maps ARM namespaces and a few full types), while resource groups and the subscription always stay. --services network therefore maps the VNets, gateways and load balancers of a subscription and nothing else.

Options and configuration keys

import asyncio

from cloudg.credentials import build_azure_credential
from cloudg.config import AzureConfig
from cloudg.inventory.azure_deep import AzureDeepInventoryCollector

credential = build_azure_credential(AzureConfig())
collector = AzureDeepInventoryCollector(
    credential,
    "00000000-0000-0000-0000-000000000000",
    services=["network", "dns"],
    exclude_services=["front_door"],
    max_child_parents=200,
)
assets = asyncio.run(collector.collect())
for record in collector.coverage.services:
    print(record.service, record.status.value, record.asset_count, record.error or "")

The keyword options are the fields of AzureDeepOptions; an unknown one raises TypeError. InventoryMapper builds them from these keys:

Key Default Effect
inventory.services / inventory.exclude_services ["all"] / [] Task and family filter (above).
inventory.kubernetes, inventory.kubernetes_timeout true, 10 In-cluster AKS mapping and its API timeout.
azure.child_resources true ARM REST child resources and subscription listings.
azure.max_child_parents 500 Most parents queried per child kind (APIs per APIM service, ...).
azure.diagnostic_settings true Read diagnostic settings of the logged resource types (children.DIAGNOSTIC_TYPES).
azure.max_diagnostic_settings 1000 Most resources whose diagnostic settings are read.
azure.data_plane true AI Search and Synapse data-plane reads.
azure.rest_concurrency 8 Concurrent ARM REST calls per subscription.
azure.map_entra true Map Entra ID through Microsoft Graph (the opt-out).
azure.entra_max_objects 10000 Most objects per Graph listing.
azure.entra_max_expansions 1000 Most groups / apps / service principals expanded one by one.

8.5 More Resource Graph tables

tables.collect_tables() runs after the four original queries. ARM-shaped rows go through add_rows(), so they meet the extractors registered for their type; the aggregate tables have handlers instead, because one asset per row would flood the map.

Task Table and types What it adds
management_group_role_assignments authorizationresources, scope a management group or /, run tenant-wide Assignments on the subscription's ancestor groups (from managementGroupAncestorsChain) as GRANTS_ACCESS grants with inherited_from: "management group". Assignments on other groups are dropped, and so are all group assignments when the chain is unknown.
role_definitions authorizationresources role definitions, tenant-wide IAM_POLICY per assigned role and per custom role assignable here, with actions, data_actions, privileged; each principal holding it gets IAM_POLICY_ATTACHMENT with USER_HAS_POLICY, GROUP_HAS_POLICY or ROLE_HAS_POLICY.
policy policyresources assignments and exemptions, then the definitions they use GUARDRAIL assignments and exemptions (same shape and ID as the tenant hierarchy builds), ORG_POLICY definitions and initiatives. A definition whose effective effect is deny or denyAction, assigned with enforcement on, GOVERNS / SCP_RESTRICTS the assignment scope. The effect is resolved through the assignment parameters, then the definition defaults; an initiative restricts when any member does.
policy_states policyresources non-compliant policystates Assignment GOVERNS / FINDING_AFFECTS each non-compliant resource (500 per assignment), and non_compliant_resources on the assignment.
security_assessments securityresources assessments and sub-assessments One SECURITY_HUB asset, cloudg:azure:defender-cspm:<sub>, MONITORS each assessed resource: FINDING_AFFECTS with unhealthy, high, titles, vulnerabilities and image names when something is unhealthy, MONITORED_BY otherwise.
secure_score securityresources secure score and controls secure_score and the 30 worst secure_score_controls on the same asset.
security_alerts securityresources active alerts THREAT_DETECTOR cloudg:azure:defender-alerts:<sub> MONITORS / FINDING_AFFECTS each resource an alert names.
backup_items recoveryservicesresources Protected items and backup instances: resource BACKUP_TO vault, policy (or vault) MANAGES / BACKUP_TO resource. Site Recovery items: source REPLICATES_TO recovery VM, network or resource group. Backup policies as BACKUP_PLAN.
kubernetes_configuration kubernetesconfigurationresources Cluster extensions (CLUSTER_ADDON) and Flux configurations (DEPLOYMENT_GROUP) contained by their cluster; Flux READS_FROM its Git repository and MANAGES the cluster.
network_resources networkresources Virtual Network Manager rows (see the REST task network_manager).
guest_configuration guestconfigurationresources GUARDRAIL per assignment, GOVERNS the machine (FINDING_AFFECTS when non-compliant).
maintenance maintenanceresources assignments Maintenance configuration MANAGES / SCHEDULED_BY the resource.
insights insightsresources data collection rule associations Resource LOGS_TO the rule, rule MONITORS the resource.

Defender plans gained two things in the builder. An enabled plan now also MONITORS / MONITORED_BY every resource of the types it covers (security.PLAN_RESOURCE_TYPES), except resources whose own resource-level pricing is Free. When the pricing query answered, every plan in security.STANDARD_PLANS that the subscription does not list gets a "not enabled" placeholder, cloudg:azure:defender-<plan>:<sub>:not-enabled, built like the AWS _disabled_asset.

8.6 ARM REST child resources

ARM answers child listings with the same ARM-shaped JSON Resource Graph returns, so the rows feed add_row() unchanged. children.CHILD_SPECS is the plan, one ChildSpec per kind:

ChildSpec(
    "service_bus",                       # task
    ("microsoft.servicebus/namespaces",),  # parent types, or SUBSCRIPTION
    "topics",                            # path under the parent ID
    "2021-11-01",                        # api-version
    nested=(ChildSpec("service_bus", (), "subscriptions", "2021-11-01"),),
)

fetch_children() picks the parent drafts (filtered by the spec's when, capped at max_child_parents), issues the GETs in a thread pool and applies the rows on the calling thread, then runs nested specs against the children it just created. Rows that only link other assets (static members of a network group, Service Bus product APIs, VNet links of a forwarding ruleset, job schedules, Stream Analytics inputs and outputs, function site config, ...) are registered with @relation_row(type): add_row() hands them to the handler and creates no asset.

The REST client (rest.AzureRestClient) uses the azure-identity credential cloudg already holds, issues only GET (and POST for the read-only listClusterUserCredential), follows nextLink and @odata.nextLink only on its own host, retries 429 and 503 after Retry-After, and paces every call through the shared rate limiter under the arm, msgraph and dataplane buckets. It is injected through arm_client_factory, so tests never touch the network.

Secrets stay out of the map. Key Vault secret values, APIM backend credentials and named-value values, API connection parameters, Data Factory typeProperties, AML datastore credentials, AI Search data source credentials, storage local-user keys and action group e-mail addresses and phone numbers are scanned for endpoints where useful and then dropped from properties.

8.7 Data planes

dataplane.fetch_data_planes() reads AI Search data sources, indexers, indexes and skillsets (https://<service>.search.windows.net, token audience https://search.azure.com/.default) and Synapse linked services, datasets, pipelines and triggers (the workspace dev endpoint, audience https://dev.azuresynapse.net/.default). Rows get synthetic ARM-style IDs under their service and go through the same extractors as Data Factory. The calls need a data-plane reader role; without one the task is PARTIAL or FAILED and the ARM view of the service is unchanged.

8.8 Entra ID

azure_entra.discover_entra(credential, client_factory=None, *, coverage, max_objects, max_expansions) runs once per mapping run from InventoryMapper._discover_hierarchies() when azure.map_entra is on and the entra task is selected. Every listing is its own coverage record:

Record Graph call Mapped as
entra_tenant /organization ORGANIZATION entra:tenant/<id>
entra_users /users IDENTITY_USER entra:principal/<oid> (guest, user_principal_name, ...)
entra_groups /groups IDENTITY_GROUP; role_assignable, dynamic
entra_group_members /groups/{id}/members group CONTAINS member
entra_applications /applications APP_REGISTRATION entra:application/<oid>; secret and certificate counts and latest expiry, requested permissions, multi_tenant
entra_application_owners /applications/{id}/owners owner MANAGES / OWNED_BY app
entra_federated_credentials /applications/{id}/federatedIdentityCredentials issuer IAM_TRUST app: ROLE_ASSUMES_ROLE for AKS issuers, CROSS_ACCOUNT_TRUST for the rest
entra_service_principals /servicePrincipals SERVICE_PRINCIPAL entra:principal/<oid>, external_application when another tenant owns the app; app MANAGES / OWNED_BY its service principal. Managed identities are skipped: the resource or user-assigned identity owning them already carries that principal.
entra_service_principal_owners /servicePrincipals/{id}/owners owner MANAGES / OWNED_BY service principal
entra_app_role_assignments /servicePrincipals/{id}/appRoleAssignedTo, Microsoft Graph first principal GRANTS_ACCESS / POLICY_ALLOWS_ACTION resource service principal with role (for example RoleManagement.ReadWrite.Directory) and privileged
entra_oauth2_grants /oauth2PermissionGrants client GRANTS_ACCESS resource with delegated, scopes, consent_type; privileged for admin consent to high scopes
entra_directory_roles /roleManagement/directory/roleDefinitions, roleAssignments IAM_POLICY entra:directoryRole/<id>; principal IAM_POLICY_ATTACHMENT role and GRANTS_ACCESS tenant
entra_pim_eligibility /roleManagement/directory/roleEligibilitySchedules the same relations with eligible: true

Entra objects carry no discovered_via, so in deduplicate() they win over the entra:principal/<oid> placeholders the subscription collectors create for role assignments, and the placeholders' grants merge into them. A 403 is recorded as "Graph permission missing"; no token at all raises CredentialError and the entra record is FAILED.

Subscription vending aliases (Microsoft.Subscription/aliases) are read once per run by discover_azure_hierarchy() through ARM REST: each alias is a PROVISIONED_PRODUCT that MANAGES / OWNED_BY its subscription. A credential without vending rights maps none and reports nothing.

Azure RBAC PIM eligibility is a subscription task (pim_eligibility): it reads roleEligibilityScheduleInstances and adds the grants with eligible: true.

8.9 In-cluster AKS

azure_kubernetes.collect_aks_workloads() maps an AKS cluster through the same kubernetes.map_cluster_objects() EKS uses, with provider=CloudProvider.AZURE. The API server and CA come from the kubeconfig of listClusterUserCredential (only the clusters section is parsed), and the bearer token is an Entra token for the AKS server application (6dae42f8-4368-4678-94ff-3960e28e3630/.default), as kubelogin gets it. Service accounts with the azure.workload.identity/client-id annotation ASSUMES_ROLE / RUNS_ON the user-assigned identity with that client ID (alias azure-client-id:<id>), and LoadBalancer services resolve their ingress IP to the public IP asset.

Clusters without Microsoft Entra integration are SKIPPED: their user credentials are secrets. Private clusters are tried and come out FAILED when cloudg cannot reach the API server. The AKS run-command (az aks command invoke) is not used, because it starts a pod in the cluster.

Everything AKS exposes through ARM is mapped as well: OIDC issuer (an alias, so federated credentials resolve to the cluster), workload identity, Entra admin groups (GRANTS_ACCESS with role: "cluster-admin"), trusted access role bindings, extensions and Flux.

8.10 Relations, AWS to Azure

The Azure extractors reuse the relationship names AWS uses for the same idea. No new RelationType members were needed.

Concept AWS source Azure source Relation
Backup protection Backup plan, selection Recovery Services / Backup vault protected items, NetApp backup BACKUP_TO
Replication RDS global DB, DynamoDB replicas, S3 replication SQL geo-replicas and failover groups, Cosmos regions, storage object replication, ACR geo-replication, App Configuration replicas, NetApp, Site Recovery REPLICATES_TO
Streaming export Firehose, log subscription filters Event Hubs Capture, diagnostic settings to Event Hubs, data collection rules, Defender continuous export STREAMS_TO
Data movement CodePipeline, Glue Data Factory and Synapse pipelines, Stream Analytics, function bindings, AML datastores, AI Search indexers READS_FROM / WRITES_TO
Secret rotation Secrets Manager rotation Lambda Event Grid near-expiry subscription on a vault ROTATES_SECRET
Schedules SSM associations, maintenance windows Function timers, Logic Apps recurrence, Automation schedules and update deployments, ADF triggers, maintenance assignments, NetApp snapshot policies MANAGES / SCHEDULED_BY
Org guardrails SCP Deny-effect policy definition GOVERNS / SCP_RESTRICTS
Permission limits Permissions boundary Deny assignment PERMISSION_BOUNDARY_LIMITS
Policy attachments IAM managed policies RBAC role definitions, Entra directory roles ROLE_HAS_POLICY, USER_HAS_POLICY, GROUP_HAS_POLICY
Cross-tenant trust Role trust of another account, RAM Lighthouse, managed application publishers, gallery sharing, external federated credentials, Sentinel AWS connectors CROSS_ACCOUNT_TRUST
Monitoring CloudWatch alarms Metric, log and activity log alerts, Defender plans and assessments, Sentinel connectors MONITORS / MONITORED_BY
Findings (none) Defender recommendations and alerts, non-compliant policy states, guest configuration FINDING_AFFECTS
Invocation EventBridge targets, alarm actions Action groups, alert actions, Logic Apps actions, APIM send-request, skillsets INVOKES
Fronting traffic ALB, CloudFront, API Gateway APIM APIs and backends, Front Door routes and rules, Application Gateway listeners, Traffic Manager, Static Web App backends SERVES_TRAFFIC_TO
Name resolution Route 53 records, resolver rules DNS record sets, Private Resolver forwarding rules and VNet links DNS_RESOLVED
Transit Transit Gateway, VPN, Direct Connect Virtual WAN hubs, routing intent, ExpressRoute, VPN sites, Route Server BGP peers, Virtual Network Manager TRANSIT_ROUTED