Inventory Azure AI Foundry Like a Boss (Projects, Endpoints, Creators + CSV)

 



Inventory Azure AI Foundry Like a Boss (Projects, Endpoints, Creators + CSV)

If you’re the Azure admin everyone pings when “where did that AI project go?”—this one’s for you. The script below scans every subscription you can see, finds all Azure AI Foundry projects, captures who created them (from ARM systemData), builds the project endpoint, pulls account keys (masked on screen, full in CSV), prints a clean colored table, and exports a ready-to-filter CSV.


What you’ll get

  • Console table (keys masked): Account, Project, Location, CreatedBy, CreatedAt (UTC), Endpoint, Key1/Key2 (masked)

  • CSV saved to C:\scripts\ai-foundry-projects-createdby-<timestamp>.csv with full values:

    • Project, ProjectEndpoint

    • CreatedBy, CreatedByType, CreatedAt, LastModifiedBy, LastModifiedAt

    • AccountKey1, AccountKey2


Prerequisites

  • PowerShell: 7.x recommended (Windows PowerShell 5.1 works too)

  • Azure permissions:

    • To discover projects: Reader or higher on target subscriptions/resource groups

    • To export account keys: a role that includes
      Microsoft.CognitiveServices/accounts/listKeys/action
      (e.g., Owner, Contributor, or Cognitive Services Contributor) on each account

  • Network: Allow outbound to Azure Resource Manager (management.azure.com)


One-time setup (modules)

Open an elevated PowerShell and run:

# Allow running local scripts (current user only)
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force

# Make sure PowerShellGet can install modules (optional but helpful)
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force

# Install the Azure modules this script uses
Install-Module Az.Accounts -Scope CurrentUser -Force -AllowClobber
Install-Module Az.Resources -Scope CurrentUser -Force -AllowClobber

Tip: If you’ve never connected to Azure from this machine, Connect-AzAccount will open a sign-in prompt the first time you run the script.


Copy-paste script

Save as Export-AIF-Projects.ps1 and run it as your Azure admin identity.

param(
  [string]$OutFolder = 'C:\scripts'
)

$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Info($m){ Write-Host "[INFO]  $m" -ForegroundColor Cyan }
function Warn($m){ Write-Host "[WARN]  $m" -ForegroundColor Yellow }

# --- Ensure modules ---
$need = @('Az.Accounts','Az.Resources')
$miss = $need | Where-Object { -not (Get-Module -ListAvailable $_) }
if ($miss) { Install-Module Az -Scope CurrentUser -Force -AllowClobber }
Import-Module Az.Accounts -ErrorAction Stop
Import-Module Az.Resources -ErrorAction Stop

# --- Output folder ---
if (-not (Test-Path $OutFolder)) { New-Item -ItemType Directory -Path $OutFolder | Out-Null }
Info "CSV will be exported to: $OutFolder"

# --- Sign in ---
Info "Signing in…"
Connect-AzAccount | Out-Null

# --- Helpers ---
function Normalize-ProjectName([string]$Name){ ($Name -split '/')[ -1 ] }
function Build-ProjectEndpoint([string]$Account,[string]$Project){
  "https://$($Account.ToLowerInvariant()).services.ai.azure.com/api/projects/$([uri]::EscapeDataString($Project))"
}

# List projects under an account (ARM; GA with fallbacks)
function Get-FoundryProjectsForAccount {
  param([string]$SubscriptionId,[string]$ResourceGroup,[string]$AccountName)
  $apis = @("2025-06-01","2024-10-01-preview","2024-05-01-preview")
  foreach ($api in $apis) {
    $url = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.CognitiveServices/accounts/$AccountName/projects?api-version=$api"
    try {
      $r = Invoke-AzRestMethod -Method GET -Uri $url
      if ($r.StatusCode -eq 200) { return ($r.Content | ConvertFrom-Json).value }
    } catch { }
  }
  @()
}

# Get one project's systemData (createdBy, createdAt, etc.)
function Get-ProjectSystemData {
  param([string]$SubscriptionId,[string]$ResourceGroup,[string]$AccountName,[string]$ProjectName)
  $apis = @("2025-06-01","2024-10-01-preview","2024-05-01-preview")
  foreach ($api in $apis) {
    $url = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.CognitiveServices/accounts/$AccountName/projects/$([uri]::EscapeDataString($ProjectName))?api-version=$api"
    try {
      $r = Invoke-AzRestMethod -Method GET -Uri $url
      if ($r.StatusCode -eq 200) { return (($r.Content | ConvertFrom-Json).'systemData') }
    } catch { }
  }
  $null
}

# Accounts – List Keys (management plane)
function Get-AccountKeys {
  param([string]$SubscriptionId,[string]$ResourceGroup,[string]$AccountName)
  $api = "2025-06-01"
  $url = "https://management.azure.com/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroup/providers/Microsoft.CognitiveServices/accounts/$AccountName/listKeys?api-version=$api"
  try {
    $r = Invoke-AzRestMethod -Method POST -Uri $url
    if ($r.StatusCode -eq 200) { return ($r.Content | ConvertFrom-Json) }
  } catch {
    $code=$null; try{$code=[int]$_.Exception.Response.StatusCode}catch{}
    Warn "Could not read keys for account '$AccountName' (HTTP $code). Need 'Microsoft.CognitiveServices/accounts/listKeys/action'."
  }
  $null
}

# Mask keys for console
function Mask-Key([string]$k){
  if (-not $k -or $k.Length -lt 8) { return $k }
  $k.Substring(0,4) + '***' + $k.Substring($k.Length-3,3)
}

# Pretty console table
function Show-ColoredTable {
  param([Parameter(Mandatory)]$Rows)
  $headerColor = 'Cyan'
  $rowColors   = @('Gray','White')
  Write-Host ""
  Write-Host ("{0,-16} {1,-22} {2,-11} {3,-24} {4,-19} {5,-58} {6,-12} {7,-12}" -f `
              'Account','Project','Location','CreatedBy','CreatedAt(UTC)','Endpoint','Key1','Key2') -ForegroundColor $headerColor
  Write-Host ('-'*180) -ForegroundColor DarkGray
  $i=0
  foreach ($r in $Rows) {
    $fg = $rowColors[$i % $rowColors.Count]
    $k1 = Mask-Key $r.AccountKey1; $k2 = Mask-Key $r.AccountKey2
    Write-Host ("{0,-16} {1,-22} {2,-11} {3,-24} {4,-19} {5,-58} {6,-12} {7,-12}" -f `
      $r.Account, $r.Project, $r.Location, $r.CreatedBy, $r.CreatedAt, $r.ProjectEndpoint, $k1, $k2) -ForegroundColor $fg
    $i++
  }
}

# --- Collect ---
$rows = New-Object System.Collections.Generic.List[object]
$subs = Get-AzSubscription
foreach ($sub in $subs) {
  Info "Scanning subscription: $($sub.Name) [$($sub.Id)]"
  Select-AzSubscription -SubscriptionId $sub.Id | Out-Null

  $accounts = Get-AzResource -ResourceType "Microsoft.CognitiveServices/accounts" -ExpandProperties
  if (-not $accounts) { Info "No Cognitive Services accounts in $($sub.Name)."; continue }

  foreach ($acct in $accounts) {
    $rg        = $acct.ResourceGroupName
    $name      = $acct.Name
    $location  = $acct.Location

    # Account keys once per account
    $keys = Get-AccountKeys -SubscriptionId $sub.Id -ResourceGroup $rg -AccountName $name

    # Projects
    $projects = Get-FoundryProjectsForAccount -SubscriptionId $sub.Id -ResourceGroup $rg -AccountName $name
    if (-not $projects -or $projects.Count -eq 0) {
      # still output a row to show keys for the account
      $rows.Add([pscustomobject]@{
        SubscriptionName = $sub.Name
        SubscriptionId   = $sub.Id
        ResourceGroup    = $rg
        Location         = $location
        Account          = $name
        Project          = ''
        ProjectEndpoint  = ''
        CreatedBy        = ''
        CreatedByType    = ''
        CreatedAt        = ''
        LastModifiedBy   = ''
        LastModifiedAt   = ''
        AccountKey1      = $keys.key1
        AccountKey2      = $keys.key2
      }) | Out-Null
      continue
    }

    foreach ($p in $projects) {
      $proj = Normalize-ProjectName $p.name
      $endpoint = Build-ProjectEndpoint -Account $name -Project $proj

      $sys = Get-ProjectSystemData -SubscriptionId $sub.Id -ResourceGroup $rg -AccountName $name -ProjectName $proj
      $createdBy      = $sys.createdBy
      $createdByType  = $sys.createdByType
      $createdAtUtc   = $null; if ($sys.createdAt) { $createdAtUtc = ([datetime]$sys.createdAt).ToUniversalTime().ToString("yyyy-MM-dd HH:mm") }
      $lastBy         = $sys.lastModifiedBy
      $lastAtUtc      = $null; if ($sys.lastModifiedAt) { $lastAtUtc = ([datetime]$sys.lastModifiedAt).ToUniversalTime().ToString("yyyy-MM-dd HH:mm") }

      $rows.Add([pscustomobject]@{
        SubscriptionName = $sub.Name
        SubscriptionId   = $sub.Id
        ResourceGroup    = $rg
        Location         = $location
        Account          = $name
        Project          = $proj
        ProjectEndpoint  = $endpoint
        CreatedBy        = $createdBy
        CreatedByType    = $createdByType
        CreatedAt        = $createdAtUtc
        LastModifiedBy   = $lastBy
        LastModifiedAt   = $lastAtUtc
        AccountKey1      = $keys.key1
        AccountKey2      = $keys.key2
      }) | Out-Null
    }
  }
}

# --- Output ---
if ($rows.Count -eq 0) {
  Warn "No AI Foundry projects/accounts found."
  return
}

Show-ColoredTable -Rows $rows

$csv = Join-Path $OutFolder ("ai-foundry-projects-createdby-{0}.csv" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
$rows |
  Select-Object SubscriptionName,SubscriptionId,ResourceGroup,Location,Account,Project,ProjectEndpoint,CreatedBy,CreatedByType,CreatedAt,LastModifiedBy,LastModifiedAt,AccountKey1,AccountKey2 |
  Export-Csv -NoTypeInformation -Encoding UTF8 $csv

Info "Exported CSV: $csv"

How to run

# If you skipped the one-time setup, run those steps first.
# Then execute:
.\Export-AIF-Projects.ps1

When it completes, look for:

[INFO]  Exported CSV: C:\scripts\ai-foundry-projects-createdby-YYYYMMDD-HHmmss.csv

Open that in Excel/Power BI and filter by subscription, project, creator, or endpoint.


Troubleshooting quickies

  • No projects appear → Ensure your signed-in identity has Reader on the subscriptions/resource groups.

  • Keys are blank → You’re missing accounts/listKeys rights on the Cognitive Services account; grant Contributor or a role that includes that action.

  • Need non-interactive (app-only) auth? → Use a service principal with Connect-AzAccount -ServicePrincipal …. I can share an app-only variant if you want.


Comments

Popular posts from this blog

Bridging the Impossible: Connecting Jira On-Prem to Power Automate & Copilot Studio — The Solution Nobody Built Until Now"

How I Automated My Entire SharePoint Tenant with 150 MCP Tools and Claude Desktop

Azure Management MCP Server