Appendix
Manual connection and CLI validation
Some change-control processes need the commands recorded before the access is granted. This is that path, with the az probes that prove it worked.
Validated against the Azure portal on 27 July 2026.
Use this page for one of three reasons:
- your change-control process needs the exact commands or portal path recorded before the access is granted;
- your security review wants a transcript proving the granted access works, taken by your own team rather than reported by a wizard;
- an admin cannot reach the wizard, and Infralign has to register the connection for you.
Everything here produces the same result as the wizard: an app registration holding Reader and Cost Management Reader, proven against Azure. The wizard still has to run afterwards, because it is what stores the credential.
Azure CLI alternative for the app registration
Section titled “Azure CLI alternative for the app registration”Use this Bash procedure instead of the portal procedure for app creation and role assignment. Create the client secret in the portal.
Sign in as a user who can register applications and assign roles. The following block is runnable after you replace the example subscription ID.
SUBSCRIPTION_ID="replace-with-subscription-id"
# Create the app registration and its service principal, capturing the appIdappId=$(az ad app create --display-name infralign-reader --query appId -o tsv)az ad sp create --id "$appId"
# Role assignments target the service principal's object id, not the appIdspObjectId=$(az ad sp show --id "$appId" --query id -o tsv)
# Grant the two required roles, per subscriptionaz role assignment create \ --assignee-object-id "$spObjectId" \ --assignee-principal-type ServicePrincipal \ --role "Cost Management Reader" \ --scope "/subscriptions/$SUBSCRIPTION_ID"
az role assignment create \ --assignee-object-id "$spObjectId" \ --assignee-principal-type ServicePrincipal \ --role "Reader" \ --scope "/subscriptions/$SUBSCRIPTION_ID"
# Confirm the assignments landedaz role assignment list --assignee "$appId" --all --output tableRepeat both assignments for every subscription you want analysed. To cover the whole estate in two commands, replace the scope with a management group:
MG_ID="replace-with-management-group-id"--scope "/providers/Microsoft.Management/managementGroups/$MG_ID"Done when the final command lists Cost Management Reader and Reader for each subscription.
Portal checks
Section titled “Portal checks”- On each subscription, open Access control (IAM) → Role assignments. Confirm
infralign-readerappears twice, once as Cost Management Reader and once as Reader. - On the app registration, open Certificates & secrets. Confirm a client secret is listed with an expiry in the future. An expired secret authenticates nothing.
az CLI validation runbook
Section titled “az CLI validation runbook”The wizard runs the equivalent of probes 1 to 4 automatically and shows a green row for each. Run them by hand when you need the output for a change record, or when you want to separate an Azure problem from a wizard problem.
Set these Bash variables before starting. The block is runnable after you replace the example values.
APP_ID="replace-with-application-client-id"TENANT_ID="replace-with-tenant-id"SUBSCRIPTION_ID="replace-with-subscription-id"RESOURCE_GROUP="replace-with-resource-group"VM_NAME="replace-with-vm-name"START_DATE="2026-07-01"END_DATE="2026-07-07"Probes 1 to 4 are the gate. Run the optional diagnostics only when a dashboard tile is empty.
If a probe returns AuthorizationFailed, the corresponding role is missing or still propagating. After a new assignment, wait up to ten minutes before retrying.
-
Sign in as the service principal. Read the secret into a variable so it does not enter shell history.
Terminal window read -rsp "client secret: " SP_SECRET; echoaz login --service-principal -u "$APP_ID" -p "$SP_SECRET" --tenant "$TENANT_ID"Done when the output lists each granted subscription. This output is illustrative:
[ { "cloudName": "AzureCloud", "id": "00000000-0000-0000-0000-000000000000", "state": "Enabled","user": { "name": "11111111-1111-1111-1111-111111111111", "type": "servicePrincipal" } } ] -
Set the subscription context. Run the probes against one subscription at a time.
Terminal window az account set --subscription "$SUBSCRIPTION_ID"Done when the command returns without an error.
-
Check the management token. A future
expiresOnconfirms the credential can call Azure Resource Manager.Terminal window az account get-access-token --resource https://management.azure.com --query expiresOnDone when the command returns a future expiry.
-
List the role assignments. Both roles should appear, scoped to the subscription.
Terminal window az role assignment list --assignee "$APP_ID" --all -o tablePrincipal Role Scope----------------- ---------------------- -------------------------------------infralign-reader Cost Management Reader /subscriptions/00000000-0000-0000-0000-000000000000infralign-reader Reader /subscriptions/00000000-0000-0000-0000-000000000000Done when both rows appear for the selected subscription.
Per-source probes: run these if a dashboard tile comes up empty
-
Probe cost (Cost Management Reader). This validates the cost data-plane. The nightly run calls
generateCostDetailsReport(api-version2024-08-01), covered by the same role.Terminal window az rest --method post \--url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.CostManagement/query?api-version=2023-11-01" \--body '{"type":"ActualCost","timeframe":"MonthToDate","dataset":{"granularity":"Daily","aggregation":{"totalCost":{"name":"Cost","function":"Sum"}}}}'The following successful output is illustrative:
{ "properties": { "columns": [ {"name":"Cost","type":"Number"},{"name":"UsageDate","type":"Number"}, {"name":"Currency","type":"String"} ],"rows": [ [3.14729643469101, 20260710, "EUR"],[6.3827424306758, 20260711, "EUR"], … ] } }A failure here names the action
Microsoft.CostManagement/query/action.Optionally, exercise the exact endpoint the nightly run uses:
Terminal window az rest --method post \--url "https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.CostManagement/generateCostDetailsReport?api-version=2024-08-01" \--body "{\"metric\":\"ActualCost\",\"timePeriod\":{\"start\":\"$START_DATE\",\"end\":\"$END_DATE\"}}"Expected result: HTTP 202 with a
Locationheader to poll. -
Probe Resource Graph (Reader). The extension-free
az restcall works anywhere;az graph queryis the alternative if you have theresource-graphextension.Terminal window # Extension-free: call the REST endpoint directlyaz rest --method post \--url "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01" \--body "{\"subscriptions\":[\"$SUBSCRIPTION_ID\"],\"query\":\"Resources | summarize count()\"}"Terminal window # Alternative: the extension route (idempotent add)az extension add --name resource-graphaz graph query --subscriptions "$SUBSCRIPTION_ID" -q "Resources | summarize count()"{ "count": 1, "data": [ { "count_": 17 } ], "skip_token": null, "total_records": 1 }A failure here names
Microsoft.ResourceGraph/resources/read. -
Probe the Activity Log (Reader).
Terminal window az monitor activity-log list --subscription "$SUBSCRIPTION_ID" --offset 24h --max-events 1 -o json[ { "caller": "Microsoft.Advisor", "category": { "value": "Recommendation" },"eventTimestamp": "2026-07-24T03:05:11.1570623Z", "level": "Informational", … } ]An empty list on a quiet subscription is valid. Widen
--offsetto7dor more before reading it as a failure. -
Probe Advisor (Reader). Advisor recommendations are read through Resource Graph, the same path the platform uses.
Terminal window az graph query --subscriptions "$SUBSCRIPTION_ID" \-q "advisorresources | where type == 'microsoft.advisor/recommendations' | limit 1"{ "count": 1, "data": [ { "properties": { "category": "Cost","impact": "Medium", "impactedField": "Microsoft.Compute/virtualMachines" } } ],"total_records": 1 }Empty output with HTTP 200 is valid. Advisor may not have generated recommendations yet, and the probe still proves read access.
-
Probe Monitor metrics (Reader). Point it at any cost-significant VM that is running.
Terminal window az monitor metrics list \--resource "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Compute/virtualMachines/$VM_NAME" \--metric "Percentage CPU" --interval PT1H --offset 3h -o json{ "interval": "PT1H", "namespace": "Microsoft.Compute/virtualMachines","value": [ { "name": { "value": "Percentage CPU" }, "timeseries": [ { "data": [{ "average": 36.88, "timeStamp": "2026-07-24T04:43:00Z" },{ "average": 5.98, "timeStamp": "2026-07-24T05:43:00Z" } ] } ], "unit": "Percent" } ] }An empty
timeserieson a stopped or idle VM is valid. Pick a running VM to see data.
When probes 1 to 4 pass, repeat from step 2 for the next subscription.
If sign-in is blocked by Conditional Access
Section titled “If sign-in is blocked by Conditional Access”If sign-in returns an AADSTS error, exclude infralign-reader from the blocking workload-identity policy. AADSTS53003 is one example.
If the policy is IP-bound, ask Infralign for the ingestion runtime’s egress IPs.
Done when the sign-in probe in step 1 succeeds.
If an admin cannot reach the wizard
Section titled “If an admin cannot reach the wizard”Email support@infralign.ai with the tenant ID, the application ID, and the subscription IDs.
Infralign will agree a transfer method with you in that case. On the normal path nothing is transferred out of band at all: your admin pastes the secret into the wizard over TLS, and it is stored encrypted. Security and data handling records how the stored credential is held.
The access above is only half the job. The wizard is what stores the connection, so finish at connect Azure. If a probe here failed and the cause is not obvious, troubleshooting is keyed by the error code.