Skip to content

Collector audit — AB#5658

Status: Complete 2026-07-25. First deliverable of the declarative-collector rebuild (Feature AB#5656, Epic AB#5638) — done before any schema was designed, per the feature's own instruction.

1. Method

Every collector under manifests/collectors/ was parsed with the PowerShell AST ([System.Management.Automation.Language.Parser]::ParseFile) — never by regex over the source text — using scripts/Invoke-CollectorAudit.ps1. Discovery reuses src/pipeline/Get-ScoutCollector.ps1, the single discovery implementation the deterministic- pipeline work (AB#5649) established, rather than re-walking the filesystem a second way.

For each of the 174 collectors the script structurally extracts:

  • the Azure resource type(s) its Processing branch filters $Resources by ($_.TYPE -eq '...' inside a $Resources | Where-Object {...} pipeline)
  • the row schema: the field names of the near-universal $obj = @{ 'Key' = ...; ... } hashtable literal — all 174 files use exactly this convention — plus the ordered Excel export column list from the $Exc.Add('Column') calls in the Reporting branch
  • whether it correlates against $Sub (subscription-name lookup) and/or $Retirements/$Unsupported (retirement cross-reference)
  • whether it filters $Resources by more than one resource type and then genuinely correlates between those sets — either inside the per-row loop, or by unioning several filtered sets into a combined collection before looping over that
  • whether it calls a cmdlet that reaches Azure/Graph itself (Get-Az*, Invoke-Az*, Invoke-RestMethod, Invoke-WebRequest, Get-Msol*, Get-Mg*, Invoke-Mg*) instead of shaping the $Resources array it was handed

The machine-readable result is tests/fixtures/collector-audit.json, one record per collector. Re-run the audit at any time with:

powershell
./scripts/Invoke-CollectorAudit.ps1

What counts as a standard primitive, not an escape hatch

Two patterns appear in almost every file and are treated as declarative building blocks the schema must support directly, not as evidence a collector needs an escape hatch:

PrimitivePresent inWhat it does
$Sub correlation150 / 174$sub1 = $SUB | Where-Object { $_.id -eq $1.subscriptionId } — subscription id → name lookup
Retirement/unsupported cross-reference88 / 174Looks up $1.id against $Retirements, then $Unsupported by ServiceID, and folds one-or-many retiring features/dates into a single display string
Tag expansioneffectively universalOne output row per tag on the resource (or one row with empty tag columns when there are none), gated on $InTag for whether the two Tag columns are exported at all

A collector is classified an escape hatch only when it does something a per-field expression language over a single filtered resource set cannot express.

The external-access classifier uses CommandAst names rather than a broad text or prefix match. It explicitly recognises Search-AzGraph and New-Object -Com, while excluding the in-process Get-AZSCSafeProperty, Get-AZSCIdSegment, and Get-AZSCCollectedValue helpers. Focused tests pin both failure directions so a future classifier cannot silently move pure shaping into the escape set or miss a non-Get/Invoke external dependency.

2. Headline numbers

Count
Total collectors174
Standard contract (the param($SCPath, $Sub, ...) shape)174
Pure shaping126 (72%)
Needs an escape hatch48 (28%)

Escape-hatch reasons (a collector can have more than one):

ReasonCountWhat it looks like
Cross-resource join20Filters $Resources by more than one type and correlates between the sets — either inside the per-row loop (Compute/AVD.ps1 joins hostpools → sessionhosts → VMs) or by unioning several filtered sets before looping over the union (Compute/AVDAzureLocal.ps1 combines Arc machines + HCI VM instances + a synthesized session-host set)
External access31Calls Get-Az*/Invoke-AzRestMethod/Search-AzGraph/etc. or constructs a COM object rather than shaping the $Resources it was handed — e.g. AI/MLModels.ps1 calls Invoke-AzRestMethod per workspace, Management/AllSubscriptions.ps1 calls Search-AzGraph, and Monitor/Outages.ps1 constructs HTMLFile through COM
No $Resources filter at all10Builds its row set entirely from live cmdlet output (all 10 overlap with "live cmdlet call" above) — Management/PolicyDefinitions.ps1, all four Security/Defender*.ps1, etc.

Per-category breakdown:

CategoryTotalPureEscape
AI27198
Analytics651
Compute1486
Containers642
Databases13130
Hybrid16142
Identity16160
Integration220
IoT110
Management19118
Monitor24177
Networking21138
Security514
Storage211
Web211

Databases is 100% pure shaping — every field is a direct projection or a small conditional expression over a single filtered resource type, with no joins and no live calls. That, plus a useful spread of expression complexity (conditional nulls, unit conversions, string splits, elastic-pool cross-references within the same resource), is why it is the category converted end-to-end as the schema's proof (AB#5659/AB#5660) rather than a smaller or more trivial one. Security is nearly the opposite case: 4 of its 5 collectors are Defender API wrappers with no $Resources filter at all.

Averaged over the 174 Standard-contract collectors: 18.8 processing fields and 17.4 exported Excel columns per collector (export is almost always processing-fields-minus-ID, since the internal correlation key is dropped before the sheet is written). The largest is Compute/VirtualMachine.ps1 at 59 fields — it joins five other resource types (NICs, VM extensions, disks, VNets, and two synthetic AZSC/VM/* correlation tables) plus a live Invoke-AzRestMethod call, and is comfortably the most complex single collector in the estate.

3. Full table

Class: Pure = pure shaping. Escape = needs an escape hatch, with the specific construct(s) that force it (CrossResourceJoin, LiveCmdletCall, NoResourcesFilter, UnimplementedContract — see §1 and §4). Fields/Cols are ProcessingFieldCount / ExportColumnCount from the JSON fixture.

CategoryCollectorClassFieldsColsEscape-hatch reason
AIAIFoundryHubsPure1716
AIAIFoundryProjectsPure1816
AIAppliedAIServicesPure1615
AIAzureAIPure1918
AIBotServicesPure1815
AIComputerVisionPure1918
AIContentModeratorPure1918
AIContentSafetyPure1918
AICustomVisionPure2019
AIFaceAPIPure1918
AIFormRecognizerPure1918
AIHealthInsightsPure2019
AIImmersiveReaderPure1918
AIMachineLearningPure2423
AIMLComputesEscape1513LiveCmdletCall
AIMLDatasetsEscape1210LiveCmdletCall
AIMLDatastoresEscape1311LiveCmdletCall
AIMLEndpointsEscape1311LiveCmdletCall
AIMLModelsEscape119LiveCmdletCall
AIMLPipelinesEscape1513LiveCmdletCall
AIOpenAIAccountsPure1615
AIOpenAIDeploymentsEscape1513LiveCmdletCall
AISearchIndexesEscape1411LiveCmdletCall
AISearchServicesPure2120
AISpeechServicePure1918
AITextAnalyticsPure2019
AITranslatorPure1918
AnalyticsDatabricksPure2221
AnalyticsDataExplorerClusterPure2625
AnalyticsEvtHubPure2120
AnalyticsPurviewPure2120
AnalyticsStreamanalyticsEscape3332CrossResourceJoin
AnalyticsSynapsePure2019
ComputeAvailabilitySetsPure1413
ComputeAVDEscape2726CrossResourceJoin
ComputeAVDApplicationGroupsPure1413
ComputeAVDApplicationsEscape1512LiveCmdletCall
ComputeAVDAzureLocalEscape1816CrossResourceJoin
ComputeAVDScalingPlansPure1816
ComputeAVDSessionHostsPure1917
ComputeAVDWorkspacesPure1514
ComputeCloudServicesPure1312
ComputeVirtualMachineEscape5958CrossResourceJoin; LiveCmdletCall
ComputeVirtualMachineScaleSetEscape3736CrossResourceJoin
ComputeVMDiskPure2726
ComputeVMOperationalDataEscape2928CrossResourceJoin; LiveCmdletCall
ComputeVMWarePure2625
ContainersAKSEscape5756CrossResourceJoin
ContainersAROPure3029
ContainersContainerAppPure2524
ContainersContainerAppEnvEscape1918CrossResourceJoin
ContainersContainerGroupsPure2221
ContainersContainerRegistriesPure2018
DatabasesCosmosDBPure2726
DatabasesMariaDBPure2928
DatabasesMySQLPure2928
DatabasesMySQLflexiblePure2827
DatabasesPOSTGREPure2928
DatabasesPOSTGREFlexiblePure2928
DatabasesRedisCachePure2625
DatabasesSQLDBPure2422
DatabasesSQLMIPure2120
DatabasesSQLMIDBPure1312
DatabasesSQLPOOLPure1917
DatabasesSQLSERVERPure1918
DatabasesSQLVMPure1514
HybridArcDataControllersPure1413
HybridArcExtensionsPure2221
HybridArcGatewaysPure1514
HybridArcKubernetesPure2120
HybridArcResourceBridgePure1716
HybridArcServerOperationalDataEscape2827CrossResourceJoin; LiveCmdletCall
HybridARCServersEscape4341LiveCmdletCall
HybridArcSitesPure1413
HybridArcSQLManagedInstancesPure1917
HybridArcSQLServersPure1917
HybridClustersPure2321
HybridGalleryImagesPure1817
HybridLogicalNetworksPure1918
HybridMarketplaceGalleryImagesPure2019
HybridStorageContainersPure1514
HybridVirtualMachinesPure2423
IdentityAdminUnitsPure86
IdentityAppRegistrationsPure119
IdentityConditionalAccessPure1210
IdentityCrossTenantAccessPure97
IdentityDirectoryRolesPure64
IdentityDomainsPure97
IdentityGroupsPure1210
IdentityLicensingPure119
IdentityManagedIdentitiesPure75
IdentityManagedIdsPure97
IdentityNamedLocationsPure108
IdentityPIMAssignmentsPure97
IdentityRiskyUsersPure119
IdentitySecurityPoliciesPure1311
IdentityServicePrincipalsPure119
IdentityUsersPure1412
IntegrationAPIMPure2423
IntegrationServiceBUSPure1615
IoTIOTHubsPure2019
ManagementAdvisorScorePure119
ManagementAllSubscriptionsEscape1312LiveCmdletCall
ManagementAutomationAccountsEscape1817CrossResourceJoin
ManagementBackupEscape2928CrossResourceJoin
ManagementCustomRoleDefinitionsEscape1514LiveCmdletCall; NoResourcesFilter
ManagementDevOpsAgentPoolsPure1211
ManagementDevOpsPipelinesPure1110
ManagementDevOpsProjectsPure109
ManagementDevOpsRepositoriesPure98
ManagementDevOpsServiceConnectionsPure1413
ManagementLighthouseDelegationsPure1413
ManagementMaintenanceConfigurationsPure2322
ManagementManagementGroupsEscape1111LiveCmdletCall; NoResourcesFilter
ManagementPolicyComplianceStatesEscape1818LiveCmdletCall; NoResourcesFilter
ManagementPolicyDefinitionsEscape1312LiveCmdletCall; NoResourcesFilter
ManagementPolicySetDefinitionsEscape1413LiveCmdletCall; NoResourcesFilter
ManagementRecoveryVaultPure1413
ManagementReservationRecomPure1614
ManagementSupportTicketsPure1715
MonitorActionGroupsPure1817
MonitorActivityLogAlertRulesPure1714
MonitorAppInsightsPure2219
MonitorAppInsightsAvailabilityTestsPure1813
MonitorAppInsightsContinuousExportEscape1512LiveCmdletCall
MonitorAppInsightsProactiveDetectionEscape1210LiveCmdletCall
MonitorAppInsightsWebTestsPure2017
MonitorAppInsightsWorkItemsEscape108LiveCmdletCall
MonitorAutoscaleSettingsPure2016
MonitorDataCollectionEndpointsPure1615
MonitorDataCollectionRulesPure1514
MonitorLAWorkspaceLinkedServicesEscape128LiveCmdletCall
MonitorLAWorkspaceSavedSearchesEscape118LiveCmdletCall
MonitorLAWorkspaceSolutionsPure1410
MonitorMetricAlertRulesPure1918
MonitorMonitorMetricsIngestionPure1613
MonitorMonitorPrivateLinkScopesPure1411
MonitorMonitorWorkbooksPure1511
MonitorOutagesEscape1614LiveCmdletCall
MonitorResourceDiagnosticSettingsPure1613
MonitorScheduledQueryRulesPure1918
MonitorSmartDetectorAlertRulesPure1613
MonitorSubscriptionDiagnosticSettingsEscape1110LiveCmdletCall; NoResourcesFilter
MonitorWorkspacesPure1615
NetworkingApplicationGatewaysEscape2423CrossResourceJoin
NetworkingAzureFirewallEscape3231CrossResourceJoin
NetworkingBastionHostsPure1514
NetworkingConnectionsPure2928
NetworkingExpressRoutePure1918
NetworkingFrontdoorPure2018
NetworkingLoadBalancerPure1918
NetworkingNATGatewayPure1615
NetworkingNetworkInterfaceEscape2726CrossResourceJoin
NetworkingNetworkSecurityGroupEscape2524CrossResourceJoin
NetworkingNetworkWatchersEscape1514CrossResourceJoin
NetworkingPrivateDNSEscape1413CrossResourceJoin
NetworkingPrivateEndpointEscape1817CrossResourceJoin
NetworkingPublicDNSPure1413
NetworkingPublicIPPure1817
NetworkingRouteTablesPure1716
NetworkingTrafficManagerPure1413
NetworkingVirtualNetworkPure2322
NetworkingVirtualNetworkGatewaysPure3332
NetworkingVirtualWANEscape2524CrossResourceJoin
NetworkingvNETPeeringPure1918
SecurityDefenderAlertsEscape1615LiveCmdletCall; NoResourcesFilter
SecurityDefenderAssessmentsEscape1615LiveCmdletCall; NoResourcesFilter
SecurityDefenderPricingEscape1211LiveCmdletCall; NoResourcesFilter
SecurityDefenderSecureScoreEscape1211LiveCmdletCall; NoResourcesFilter
SecurityVaultPure2221
StorageNetAppPure2625
StorageStorageAccountsEscape4140LiveCmdletCall
WebAPPServicePlanEscape2120CrossResourceJoin
WebAPPServicesPure3736

4. Removed dead registration-contract collectors

Identity/IdentityProviders.ps1 and Identity/SecurityDefaults.ps1 were removed during the v3 rebuild. They depended on a registration API that was only mocked by tests and was never implemented by the module, so neither could produce a row in a shipped run. The processing and reporting special cases were removed with them. The Entra extraction capability remains separate from this retired inventory-collector contract.

5. What this means for the schema (AB#5657)

  • The majority declarative surface the schema must cover, in order of how often it appears: a single resource-type filter, a row-per-match with a fixed set of named fields (mostly direct $data.<property> projections, some unit conversions and string formatting), the $Sub lookup, the retirement cross-reference, the per-tag row expansion gated on $InTag, and the ordered Excel export column list with its worksheet name and conditional-formatting rules.
  • The escape hatch must cover, cleanly, the 46 real cases (excluding the 2 recommended for deletion): multi-resource-type joins/unions and calls to live Azure/Graph cmdlets. Both need the full $Resources/$Sub/$Retirements/$Unsupported context a Standard collector already receives — the escape hatch is not a smaller sandbox, it is a named script block that gets the same inputs a hand-written collector always had.
  • The proof of the schema converts Databases (13 collectors, 100% pure shaping, 13–29 fields each) end-to-end, per AB#5659/AB#5660.

Released under the MIT License.