Cost and Inventory Export Service

CIES

CIES scans your Azure environment with Resource Graph and Cost Management, then publishes what it finds as a report that refreshes on its own — so the answer to “what do we have, and what does it cost” is always sitting somewhere, instead of living in whoever last exported a spreadsheet.

Granting Graph permissions
Overview

What CIES is

CIES runs a set of KQL queries against Azure Resource Graph and Azure Cost Management across the subscriptions you point it at, then publishes the results as a structured report — resource inventory, cost breakdowns, network topology, KeyVault access, App Registration credential expiry, Service Bus health, Azure Monitor alert coverage, and configuration drift. Each scheduled run overwrites the last, so the report is never more than one cycle out of date.

Deployed as an Azure Marketplace Solution Template, it runs with no App Registration and no client secret: a scheduled Container Apps Job authenticates as its own system-assigned managed identity, and the report is published to a Static Web App in a resource group you own.

The problem

What it solves

Most teams don’t lack Azure data — they lack a standing place to look at it. The usual substitute is a one-off export or a spreadsheet that’s accurate for exactly as long as nobody deploys anything new.

nobody notices spend creep until finance escalates

Cost blind spots

Resource counts and spend drift apart from what anyone remembers approving.

With CIES: a recurring, cost-enriched inventory tied to real Resource Graph data.

forgotten resources and silent config changes pile up

Environment drift

Test resources outlive their purpose; configuration quietly diverges from what was intended.

With CIES: a dedicated drift report surfaces what changed.

a secret expires in production with no warning

Credential risk

App Registration secrets and certificates expire on their own schedule, not yours.

With CIES: an expiry-monitored App Registration inventory.

privileged access history is scattered or unavailable

Privileged access visibility

Who activated what, and whether it was approved, is hard to reconstruct after the fact.

With CIES: PIM request history merged with its approval outcome.

It’s built for the people who own that visibility gap:

Cloud & platform teams FinOps Security & compliance
Setup — optional

Granting Microsoft Graph permissions

On a Marketplace deployment, the App Registration Inventory and PIM Activity sections come back empty by default: the managed identity only holds Reader on your subscriptions, and these two sections additionally need Microsoft Graph permissions the deployment wizard can’t request on your behalf. That’s expected — not a broken job. Grant the five permissions below to populate them.

!

Before you start: you need to be signed in to the Azure Portal as a Privileged Role Administrator, Global Administrator, or Cloud Application Administrator. These are the only roles that can grant this permission.

i

Everything below stays inside portal.azure.com. The one part that needs a command instead of a button runs in the Portal’s own Cloud Shell — nothing is installed on your machine.

Find the managed identity’s object ID

You’ll need this ID to target the script at the right identity.

  1. In the Azure Portal, open the resource group the offer was deployed into.
  2. Open the Container Apps Job it created (its name starts with your deploy-time prefix).
  3. Select Identity in the left-hand menu.
  4. Copy the Object (principal) ID — you’ll paste it into the script in step 3.

Open Cloud Shell

Click the Cloud Shell icon in the Portal’s top toolbar, and choose PowerShell if prompted. First time in this tenant, follow its one-time storage prompt — that only happens once.

Run the script

Paste the script below, replacing the placeholder with the object ID from step 1. It looks up what’s already granted and only adds what’s missing, so it’s safe to run more than once. All five permissions are read-only — this cannot let the identity modify app registrations, roles, group memberships, or PIM assignments.

Cloud Shell — PowerShell
$miObjectId = "<object-id-from-step-1>"

# Installs the Graph module if it isn't already present in this Cloud Shell session
if (-not (Get-Module -ListAvailable Microsoft.Graph.Applications)) {
    Install-Module Microsoft.Graph.Applications -Scope CurrentUser -Force
}
Connect-MgGraph -Scopes "AppRoleAssignment.ReadWrite.All","Application.Read.All" -NoWelcome

# The Microsoft Graph service principal, and what the identity already holds on it
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"
$existing = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $miObjectId |
    Where-Object { $_.ResourceId -eq $graphSp.Id } |
    Select-Object -ExpandProperty AppRoleId

$roles = @{
    "Directory.Read.All"                             = "7ab1d382-f21e-4acd-a863-ba3e13f7da61"
    "AuditLog.Read.All"                               = "b0afded3-3588-46d8-8b3d-9842eff778da"
    "RoleManagement.Read.Directory"                   = "483bed4a-2ad3-4361-a73b-c83ccdbdc53c"
    "PrivilegedAssignmentSchedule.Read.AzureADGroup"  = "cd4161cb-f098-48f8-a884-1eda9a42434c"
    "PrivilegedEligibilitySchedule.Read.AzureADGroup" = "8f44f93d-ecef-46ae-a9bf-338508d44d6b"
}

# Grants only what's missing, then reports the outcome for all five
$results = foreach ($name in $roles.Keys) {
    if ($existing -contains $roles[$name]) {
        [pscustomobject]@{ Permission = $name; Status = "Already granted" }
    } else {
        New-MgServicePrincipalAppRoleAssignment `
            -ServicePrincipalId $miObjectId `
            -PrincipalId $miObjectId `
            -ResourceId $graphSp.Id `
            -AppRoleId $roles[$name] | Out-Null
        [pscustomobject]@{ Permission = $name; Status = "Granted" }
    }
}
$results | Format-Table -AutoSize

There’s no separate admin-consent screen — this run is the grant. A successful run ends with a table showing all five permissions as Granted or Already granted.

Confirm and wait for the next run

No redeploy or restart needed — the managed identity’s next Graph API call picks up the new permissions automatically. Role assignments are typically effective within a few minutes, occasionally longer. If the two sections are still empty after the next scheduled run, wait for one more before troubleshooting.

To trigger a run immediately instead of waiting: copy the az containerapp job start command from the deployment’s Outputs tab and run it in the same Cloud Shell pane.

Reference

What each permission is for

All five are read-only (*.Read.*) — two power App Registration Inventory, three power PIM Activity.

Permission Used by Why
Directory.Read.All App Reg Inventory Reads app registrations and credential metadata.
AuditLog.Read.All App Reg Inventory Sign-in activity per app — optional, degrades gracefully if missing.
RoleManagement.Read.Directory PIM Activity Entra directory-role PIM requests: activations, eligibility, approvals.
PrivilegedAssignmentSchedule.Read.AzureADGroup PIM Activity PIM-for-Groups assignment requests.
PrivilegedEligibilitySchedule.Read.AzureADGroup PIM Activity PIM-for-Groups eligibility requests.
If something looks wrong

Troubleshooting

“Insufficient privileges” when running the script

The signed-in account doesn’t hold one of the roles listed under “Before you start.” Have someone with Global Administrator or Privileged Role Administrator run the script instead.

The sections are still empty after two scheduled runs

Re-run the script — it’s safe to run again, and its output table confirms whether all five permissions are actually present. A partial grant (e.g. only Directory.Read.All) still leaves the PIM section empty. Also check the Container Apps Job’s execution history for the specific Graph error.

I don’t have any of the required admin roles, and can’t get them

These two sections will stay empty on this deployment. Running CIES outside the Marketplace offer, with a customer-created App Registration, is the alternative path.

Every step above stays inside the Azure Portal you already deployed into — nothing is installed locally, and nothing is sent to the publisher.