faitza.com
faitza.com
> blog_tech

Azure : inventaire infrastructure via ARM & Resource Graph en PowerShell

Calcul... powershell azure arm resource-graph key-vault infra
Azure + PowerShell + ARM API

Introduction

Faitza_AZ.ps1 regroupe les fonctions PowerShell pour inventorier et gérer l'infrastructure Azure : machines virtuelles, réseau (NSG, subnets, NIC, routes), disques, tags, alertes métriques et secrets Key Vault. Toutes les requêtes d'inventaire passent par Azure Resource Graph via Invoke-Faitza_AZ_ARG — une seule requête KQL couvre toutes les souscriptions sans boucler sur les ressources une par une.

Prérequis

Les fonctions d'inventaire utilisent Get-Faitza_Modul_Headers -resource "arm", les fonctions Key Vault utilisent Get-Faitza_Modul_Headers -resource "keyvault". Voir l'article dédié : Token Bearer pour les APIs Microsoft en PowerShell.

ARG — Moteur Azure Resource Graph

Fonction centrale utilisée par toutes les autres. Envoie une requête KQL à l'API Azure Resource Graph (Microsoft.ResourceGraph/resources), gère la pagination via $skipToken et les throttling 429 avec retry automatique (jusqu'à 4 tentatives, délai Retry-After).

Droits requis

Reader sur les souscriptions ou groupes de ressources ciblés · Token arm

function Invoke-Faitza_AZ_ARG {
    param (
        [string]$Body,
        [hashtable]$Headers,
        [string]$CallerName = "?"
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Invoke-Faitza_AZ_ARG <<<$($PSStyle.Reset)"

        $Url = "https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01"
        $MaxRetries = 4
        $AllData = @()
        $CurrentBody = $Body
        $PageIndex = 0

        do {
            $PageIndex++
            $RetryCount = 0
            $Success = $false
            $Response = $null

            Write-Host "  [ARG][$CallerName] Page $PageIndex - début appel REST ($(Get-Date -Format 'HH:mm:ss'))"

            while (-not $Success -and $RetryCount -lt $MaxRetries) {
                try {
                    Write-Host "  [ARG][$CallerName] Invoke-RestMethod envoyé (tentative $($RetryCount+1)/$MaxRetries) ($(Get-Date -Format 'HH:mm:ss'))"
                    $Response = Invoke-RestMethod -Method Post -Uri $Url -Headers $Headers -Body $CurrentBody -ContentType 'application/json' -TimeoutSec 90
                    Write-Host "  [ARG][$CallerName] Réponse reçue : $(@($Response.data).Count) lignes ($(Get-Date -Format 'HH:mm:ss'))"
                    $Success = $true
                } catch {
                    $ex = $_.Exception
                    $resp = $ex.Response
                    Write-Host "  [ARG][$CallerName] Exception capturée : $($ex.GetType().Name) - $($ex.Message) ($(Get-Date -Format 'HH:mm:ss'))"
                    if ($null -ne $resp) {
                        Write-Host "  [ARG][$CallerName] HTTP StatusCode : $($resp.StatusCode) ($([int]$resp.StatusCode))"
                        Write-Host "  [ARG][$CallerName] Retry-After header : '$($resp.Headers["Retry-After"])'"
                    } else {
                        Write-Host "  [ARG][$CallerName] Pas de réponse HTTP (timeout réseau ou proxy ?)"
                    }
                    if ($null -ne $resp -and $resp.StatusCode -eq 'TooManyRequests') {
                        $RetryCount++
                        $retryAfter = $resp.Headers["Retry-After"]
                        if (-not $retryAfter) { $retryAfter = 5 }
                        Write-Host "$($PSStyle.Foreground.BrightYellow)API Azure saturée (429). Attente $retryAfter sec (tentative $RetryCount/$MaxRetries)...$($PSStyle.Reset)"
                        Start-Sleep -Seconds $retryAfter
                    } else { throw }
                }
            }

            if (-not $Success) { throw "Impossible d'interroger Azure Resource Graph après $MaxRetries tentatives (Throttling 429 persistant)." }

            if ($Response.data) { $AllData += $Response.data }
            Write-Host "  [ARG][$CallerName] Total cumulé : $($AllData.Count) lignes - skipToken présent : $($null -ne $Response.'$skipToken') ($(Get-Date -Format 'HH:mm:ss'))"

            if ($Response.'$skipToken') {
                $BodyObj = $CurrentBody | ConvertFrom-Json -AsHashtable
                $BodyObj['$skipToken'] = $Response.'$skipToken'
                $CurrentBody = $BodyObj | ConvertTo-Json -Depth 10
            }
        } while ($Response.'$skipToken')

        return $AllData
    } catch {
        throw "Erreur Invoke-Faitza_AZ_ARG : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Invoke-Faitza_AZ_ARG >>>$($PSStyle.Reset)"
    }
}
# Requête KQL personnalisée sur toutes les souscriptions
$Headers = Get-Faitza_Modul_Headers -resource "arm"
$Body = @{ query = "Resources | where type =~ 'microsoft.compute/virtualmachines' | project name, location, resourceGroup | order by name asc" } | ConvertTo-Json
$vms = Invoke-Faitza_AZ_ARG -Body $Body -Headers $Headers -CallerName "MonScript"
Write-Host "$($vms.Count) VMs trouvées"

# Limiter à une souscription spécifique
$Body = @{
    query         = "Resources | where type =~ 'microsoft.compute/virtualmachines' | project name"
    subscriptions = @("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
} | ConvertTo-Json -Depth 5
Invoke-Faitza_AZ_ARG -Body $Body -Headers $Headers

KV Read — Lire un secret Key Vault

Retourne la valeur d'un secret depuis un Azure Key Vault via l'API REST (api-version=7.4). Le vault par défaut est kv-faitza-infra, configurable via -vaultName.

Droits requis

Key Vault Secrets User sur le vault · Token keyvault

function Read-Faitza_AZ_KV {
    [CmdletBinding()]
    param (
        [string]$secret,
        [string]$vaultName = "kv-faitza-infra"
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Read-Faitza_AZ_KV <<<$($PSStyle.Reset)"
        if (-not $secret) { throw "paramètre -secret manquant" }
        Write-Host "Secret : $secret"
        $headers = Get-Faitza_Modul_Headers -resource "keyvault"
        $uri = "https://$vaultName.vault.azure.net/secrets/$secret/?api-version=7.4"
        $response = Invoke-RestMethod -Method Get -Uri $uri -Headers $headers -SkipCertificateCheck -ErrorAction Stop
        return $response.value
    } catch {
        throw "Erreur Read-Faitza_AZ_KV : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Read-Faitza_AZ_KV >>>$($PSStyle.Reset)"
    }
}
# Lire un secret depuis le vault par défaut
$password = Read-Faitza_AZ_KV -secret "db-password"

# Vault spécifique
$apiKey = Read-Faitza_AZ_KV -secret "gemini-api-key" -vaultName "kv-prod-secrets"

KV Set — Écrire un secret Key Vault

Crée ou met à jour un secret dans un Azure Key Vault via un PUT REST. Retourne l'objet de réponse complet incluant l'ID de version du secret.

Droits requis

Key Vault Secrets Officer sur le vault · Token keyvault

function Set-Faitza_AzureKeyVault_Secret {
    [CmdletBinding()]
    param (
        [string]$SecretName,
        [string]$SecretValue,
        [string]$VaultName
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Set-Faitza_AzureKeyVault_Secret <<<$($PSStyle.Reset)"

        if (-not $SecretName)  { throw "paramètre -SecretName manquant" }
        if (-not $SecretValue) { throw "paramètre -SecretValue manquant" }

        Write-Host "Enregistrement du secret : $SecretName dans le vault $VaultName"
        $headers = Get-Faitza_Modul_Headers -resource "keyvault"
        $uri = "https://$VaultName.vault.azure.net/secrets/$SecretName/?api-version=7.4"
        $body = @{ value = $SecretValue } | ConvertTo-Json -Depth 2 -Compress
        $response = Invoke-RestMethod -Method Put -Uri $uri -Headers $headers -Body $body -SkipCertificateCheck -ErrorAction Stop

        Write-Host "Secret enregistré avec succès (Version: $($response.id.Split('/')[-1]))."
        return $response
    } catch {
        throw "Erreur Set-Faitza_AzureKeyVault_Secret : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Set-Faitza_AzureKeyVault_Secret >>>$($PSStyle.Reset)"
    }
}
# Créer ou mettre à jour un secret
$result = Set-Faitza_AzureKeyVault_Secret `
    -SecretName  "api-key-prod" `
    -SecretValue "mon-secret-valeur" `
    -VaultName   "kv-faitza-infra"

Write-Host "Version : $($result.id.Split('/')[-1])"

Alerte — Alertes de métriques

Inventorie toutes les alertes de métriques Azure (microsoft.insights/metricalerts) via Resource Graph. Filtre optionnel par type de ressource cible (-Filtre) ou par nom d'objet spécifique (-Cible). Retourne le nom de l'alerte, la métrique surveillée, l'opérateur, le seuil, la sévérité et l'état activé.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_Alerte {
    param (
        [string]$Cible = "",
        [string]$Filtre = "",
        [string]$sub = ""
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_Alerte <<<$($PSStyle.Reset)"

        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.insights/metricalerts'
            | mv-expand targetScope = properties.scopes
            | extend TargetResource_ID = tostring(targetScope)
"@
        if (-not [string]::IsNullOrWhiteSpace($Filtre)) {
            Write-Host "Type de ressource ciblé : $Filtre"
            $KqlQuery += "`n| where TargetResource_ID contains '$Filtre'"
        }
        if (-not [string]::IsNullOrWhiteSpace($Cible)) {
            Write-Host "Objet spécifique ciblé : $Cible"
            $KqlQuery += "`n| where TargetResource_ID contains '$Cible'"
        }
        $KqlQuery += @"
            | extend TargetResource_Name = tostring(split(TargetResource_ID, '/')[-1])
            | extend ResourceType = tostring(split(TargetResource_ID, '/')[-2])
            | mv-expand condition = properties.criteria.allOf
            | project
                id = tolower(tostring(id)),
                ResourceType,
                TargetResource_Name,
                AlertName = name,
                MetricName = tostring(condition.metricName),
                Operator = tostring(condition.operator),
                Threshold = toreal(condition.threshold),
                Severity = properties.severity,
                Enabled = properties.enabled
            | order by ResourceType asc, TargetResource_Name asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Body = $BodyHash | ConvertTo-Json -Depth 10

        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body $Body -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_Alerte : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_Alerte >>>$($PSStyle.Reset)"
    }
}
# Toutes les alertes
$alertes = Get-Faitza_AZ_Alerte
$alertes | Format-Table AlertName, ResourceType, MetricName, Threshold, Enabled -AutoSize

# Alertes sur les VMs uniquement
Get-Faitza_AZ_Alerte -Filtre "virtualmachines" | Format-Table -AutoSize

# Alertes sur une ressource spécifique
Get-Faitza_AZ_Alerte -Cible "vm-prod-01" | Format-Table AlertName, MetricName, Operator, Threshold -AutoSize

VM — Inventaire des machines virtuelles

Retourne l'inventaire complet des VMs Azure avec leur configuration réseau résolue : IP privée, subnet, taille, OS, disque OS, nom ordinateur, nom admin, nombre de disques de données, type de licence et tags. Utilise un join KQL avec les interfaces réseau pour résoudre l'IP sans appel supplémentaire.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_VM {
    param (
        [string]$Cible = "",
        [string]$sub = ""
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_VM <<<$($PSStyle.Reset)"

        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.compute/virtualmachines'
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) {
            Write-Host "Objet spécifique ciblé : $Cible"
            $KqlQuery += "`n            | where name contains '$Cible' or id contains '$Cible' or resourceGroup contains '$Cible'"
        }
        $KqlQuery += @"
            | extend nicId = tolower(tostring(properties.networkProfile.networkInterfaces[0].id))
            | join kind=leftouter (
                Resources
                | where type =~ 'microsoft.network/networkinterfaces'
                | extend ipConfig = properties.ipConfigurations[0]
                | extend PrivateIP = tostring(ipConfig.properties.privateIPAddress)
                | extend SubnetId = tostring(ipConfig.properties.subnet.id)
                | extend SubnetName = tostring(split(SubnetId, '/')[-1])
                | project nicId = tolower(tostring(id)), PrivateIP, SubnetName
            ) on nicId
            | project
                id = tolower(tostring(id)),
                Name = name,
                PrivateIP = iff(isnotempty(PrivateIP), PrivateIP, "N/A"),
                Subnet = iff(isnotempty(SubnetName), SubnetName, "N/A"),
                ResourceGroup = resourceGroup,
                Location = location,
                Zone = tostring(zones[0]),
                VmSize = tostring(properties.hardwareProfile.vmSize),
                OsType = iff(
                    isnotempty(properties.storageProfile.imageReference.sku),
                    strcat(properties.storageProfile.imageReference.publisher, ":", properties.storageProfile.imageReference.offer, ":", properties.storageProfile.imageReference.sku, ":", properties.storageProfile.imageReference.version),
                    strcat("Custom:", properties.storageProfile.osDisk.osType)
                ),
                OsDiskType = tostring(properties.storageProfile.osDisk.managedDisk.storageAccountType),
                ComputerName = tostring(properties.osProfile.computerName),
                AdminUsername = tostring(properties.osProfile.adminUsername),
                DataDisksCount = array_length(properties.storageProfile.dataDisks),
                NetworkInterfacesCount = array_length(properties.networkProfile.networkInterfaces),
                LicenseType = tostring(properties.licenseType),
                ProvisioningState = tostring(properties.provisioningState),
                Tags = tostring(tags)
            | order by Name asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Body = $BodyHash | ConvertTo-Json -Depth 10

        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body $Body -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_VM : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_VM >>>$($PSStyle.Reset)"
    }
}
# Toutes les VMs
$vms = Get-Faitza_AZ_VM
$vms | Format-Table Name, PrivateIP, Subnet, VmSize, Location -AutoSize

# VM spécifique par nom
Get-Faitza_AZ_VM -Cible "vm-prod-01" | Format-List

# Export Excel
$vms | Export-Excel -Path ".\inventaire_vms.xlsx" -WorksheetName "VMs" -AutoSize -FreezeTopRow -TableName "TVMs"

NSG — Règles de sécurité réseau

Inventorie toutes les règles NSG et résout automatiquement les IPs sources et destinations en noms de VM ou de subnet via deux requêtes ARG supplémentaires (NIC et VNet). Les champs ResolvedSource et ResolvedDestination remplacent les CIDR par des noms lisibles.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_NSG_Rules {
    param (
        [string]$Cible = "",
        [string]$sub = ""
    )
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_NSG_Rules <<<$($PSStyle.Reset)"

        $KqlNSG = @"
            Resources
            | where type =~ 'microsoft.network/networksecuritygroups'
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlNSG += "`n            | where name contains '$Cible' or resourceGroup contains '$Cible'" }
        $KqlNSG += @"
            | mv-expand rule = properties.securityRules
            | project
                id = tolower(tostring(rule.id)),
                NSG_Name = name,
                ResourceGroup = resourceGroup,
                RuleName = tostring(rule.name),
                Priority = toint(rule.properties.priority),
                Direction = tostring(rule.properties.direction),
                Access = tostring(rule.properties.access),
                Protocol = tostring(rule.properties.protocol),
                SourcePort = tostring(rule.properties.sourcePortRange),
                DestinationPort = tostring(rule.properties.destinationPortRange),
                SourceIP = iff(array_length(rule.properties.sourceAddressPrefixes) > 0, strcat_array(rule.properties.sourceAddressPrefixes, ','), tostring(rule.properties.sourceAddressPrefix)),
                DestinationIP = iff(array_length(rule.properties.destinationAddressPrefixes) > 0, strcat_array(rule.properties.destinationAddressPrefixes, ','), tostring(rule.properties.destinationAddressPrefix)),
                SourceASGs = rule.properties.sourceApplicationSecurityGroups,
                DestinationASGs = rule.properties.destinationApplicationSecurityGroups,
                Description = tostring(rule.properties.description)
            | order by NSG_Name asc, Priority asc
"@
        $BodyNSG_HT = @{ query = $KqlNSG }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyNSG_HT.subscriptions = @($sub) }
        $BodyNSG = $BodyNSG_HT | ConvertTo-Json -Depth 10

        $KqlNIC = @"
            Resources
            | where type =~ 'microsoft.network/networkinterfaces'
            | mv-expand ipconfig = properties.ipConfigurations
            | extend privateIP = tostring(ipconfig.properties.privateIPAddress)
            | extend vmId = tostring(properties.virtualMachine.id)
            | where isnotempty(privateIP) and isnotempty(vmId)
            | project IP = privateIP, vmId = vmId
"@
        $BodyNIC_HT = @{ query = $KqlNIC }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyNIC_HT.subscriptions = @($sub) }
        $BodyNIC = $BodyNIC_HT | ConvertTo-Json -Depth 10

        $KqlSubnet = @"
            Resources
            | where type =~ 'microsoft.network/virtualnetworks'
            | mv-expand subnet = properties.subnets
            | extend subnetPrefix = tostring(subnet.properties.addressPrefix)
            | extend subnetName = tostring(subnet.name)
            | where isnotempty(subnetPrefix)
            | project IP = subnetPrefix, Name = subnetName
"@
        $BodySubnet_HT = @{ query = $KqlSubnet }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodySubnet_HT.subscriptions = @($sub) }
        $BodySubnet = $BodySubnet_HT | ConvertTo-Json -Depth 10

        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        $ReponseNSG    = Invoke-Faitza_AZ_ARG -Body $BodyNSG    -Headers $Headers
        $ReponseNIC    = Invoke-Faitza_AZ_ARG -Body $BodyNIC    -Headers $Headers
        $ReponseSubnet = Invoke-Faitza_AZ_ARG -Body $BodySubnet -Headers $Headers

        $DicoReseau = @{}
        if ($ReponseNIC) {
            foreach ($nic in $ReponseNIC) {
                if (-not [string]::IsNullOrWhiteSpace($nic.IP) -and -not [string]::IsNullOrWhiteSpace($nic.vmId)) {
                    $vmName = ($nic.vmId -split '/') | Select-Object -Last 1
                    $DicoReseau.Add($nic.IP.ToString(), $vmName)
                }
            }
        }
        if ($ReponseSubnet) {
            foreach ($subnetEntry in $ReponseSubnet) {
                if (-not [string]::IsNullOrWhiteSpace($subnetEntry.IP)) { $DicoReseau.Add($subnetEntry.IP.ToString(), $subnetEntry.Name) }
            }
        }

        $ResultatFinal = @()
        if ($ReponseNSG) {
            foreach ($rule in $ReponseNSG) {
                $srcIps = if ($rule.SourceIP) { $rule.SourceIP -split ',' } else { @() }
                $resolvedSrcArray = foreach ($ip in $srcIps) {
                    if (-not [string]::IsNullOrWhiteSpace($ip)) {
                        $ipTrim = $ip.Trim()
                        $cleanIp = $ipTrim -replace '^(.+)/32$', '$1'
                        if (-not [string]::IsNullOrWhiteSpace($cleanIp) -and $DicoReseau.ContainsKey($cleanIp)) { $DicoReseau.Item($cleanIp) } else { $ipTrim }
                    }
                }
                $rule | Add-Member -MemberType NoteProperty -Name "ResolvedSource" -Value ($resolvedSrcArray -join ', ')

                $dstIps = if ($rule.DestinationIP) { $rule.DestinationIP -split ',' } else { @() }
                $resolvedDstArray = foreach ($ip in $dstIps) {
                    if (-not [string]::IsNullOrWhiteSpace($ip)) {
                        $ipTrim = $ip.Trim()
                        $cleanIp = $ipTrim -replace '^(.+)/32$', '$1'
                        if (-not [string]::IsNullOrWhiteSpace($cleanIp) -and $DicoReseau.ContainsKey($cleanIp)) { $DicoReseau.Item($cleanIp) } else { $ipTrim }
                    }
                }
                $rule | Add-Member -MemberType NoteProperty -Name "ResolvedDestination" -Value ($resolvedDstArray -join ', ')
                $ResultatFinal += $rule
            }
        }

        $Ordre = @("id","NSG_Name","ResourceGroup","RuleName","Priority","Direction","Access","Protocol","SourcePort","DestinationPort","SourceIP","ResolvedSource","DestinationIP","ResolvedDestination","Description")
        return $ResultatFinal | Select-Object $Ordre
    } catch {
        throw "Erreur Get-Faitza_AZ_NSG_Rules : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_NSG_Rules >>>$($PSStyle.Reset)"
    }
}
# Toutes les règles NSG avec résolution des IPs
$nsgRules = Get-Faitza_AZ_NSG_Rules
$nsgRules | Format-Table NSG_Name, RuleName, Priority, Direction, Access, ResolvedSource, ResolvedDestination -AutoSize

# NSG spécifique
Get-Faitza_AZ_NSG_Rules -Cible "nsg-prod" | Where-Object Access -eq "Deny" | Format-Table -AutoSize

# Export
$nsgRules | Export-Excel -Path ".\nsg_rules.xlsx" -WorksheetName "NSG" -AutoSize -FreezeTopRow

Subnet — Sous-réseaux VNet

Liste tous les sous-réseaux de tous les VNets avec leur plage d'adresses, le NSG associé et son resource group.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_Subnet {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_Subnet <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.network/virtualnetworks'
            | mv-expand subnet = properties.subnets
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where tostring(subnet.name) contains '$Cible'" }
        $KqlQuery += @"
            | project id = tolower(tostring(subnet.id)), VNet = name, VNetRG = resourceGroup, SubnetName = tostring(subnet.name), AddressPrefix = iff(isnotempty(subnet.properties.addressPrefix), tostring(subnet.properties.addressPrefix), tostring(subnet.properties.addressPrefixes[0])), NsgRG = iff(isnotempty(subnet.properties.networkSecurityGroup), extract(@"/resourceGroups/([^/]+)", 1, tostring(subnet.properties.networkSecurityGroup.id)), "N/A"), Location = location, NSG = iff(isnotempty(subnet.properties.networkSecurityGroup), extract(@"/networkSecurityGroups/([^/]+)", 1, tostring(subnet.properties.networkSecurityGroup.id)), "N/A")
            | order by VNet asc, SubnetName asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_Subnet : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_Subnet >>>$($PSStyle.Reset)"
    }
}
# Tous les subnets
Get-Faitza_AZ_Subnet | Format-Table VNet, SubnetName, AddressPrefix, NSG -AutoSize

# Subnet spécifique
Get-Faitza_AZ_Subnet -Cible "snet-prod" | Format-List

RG — Resource Groups

Liste tous les resource groups avec leur localisation, souscription, état de provisioning et tags.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_RG {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_RG <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            ResourceContainers
            | where type =~ 'microsoft.resources/subscriptions/resourcegroups'
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where name contains '$Cible' or location contains '$Cible'" }
        $KqlQuery += @"
            | project id = tolower(tostring(id)), Name = name, Location = location, SubscriptionId = subscriptionId, ProvisioningState = tostring(properties.provisioningState), Tags = tostring(tags)
            | order by Name asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_RG : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_RG >>>$($PSStyle.Reset)"
    }
}
# Tous les RG
Get-Faitza_AZ_RG | Format-Table Name, Location, ProvisioningState -AutoSize

# RG d'une région
Get-Faitza_AZ_RG -Cible "francecentral" | Select-Object Name, SubscriptionId

Disk — Disques de données

Inventorie les disques de données managés (hors disques OS) avec leur VM attachée, numéro LUN, caching, taille, SKU et type lisible (Standard HDD, Standard SSD, Premium SSD, Ultra Disk…).

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_Disk {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_Disk <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.compute/disks'
            | where isempty(tostring(properties.osType))
            | extend diskId = tolower(id)
            | join kind=leftouter (
                Resources
                | where type =~ 'microsoft.compute/virtualmachines'
                | mv-expand dataDisk = properties.storageProfile.dataDisks
                | project diskId = tolower(tostring(dataDisk.managedDisk.id)), LUN = toint(dataDisk.lun), Caching = tostring(dataDisk.caching)
            ) on diskId
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where name contains '$Cible'" }
        $KqlQuery += @"
            | project id = diskId, DiskName = name, ResourceGroup = resourceGroup, Location = location, SizeGB = toint(properties.diskSizeGB), VMName = tostring(split(tostring(managedBy), '/')[-1]), State = tostring(properties.diskState), LUN = iff(isnotnull(LUN), tostring(LUN), ""), Caching = iff(isnotempty(Caching), Caching, "None"), Sku = tostring(sku.name), Type = case(sku.name contains "StandardSSD", "Standard SSD", sku.name contains "Standard_", "Standard HDD", sku.name contains "PremiumV2", "Premium SSD v2", sku.name contains "Premium_", "Premium SSD", sku.name contains "Ultra", "Ultra Disk", tostring(sku.name))
            | order by VMName asc, LUN asc, DiskName asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_Disk : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_Disk >>>$($PSStyle.Reset)"
    }
}
# Tous les disques de données
$disks = Get-Faitza_AZ_Disk
$disks | Format-Table DiskName, VMName, SizeGB, Type, State, LUN -AutoSize

# Disques non attachés (orphelins)
$disks | Where-Object State -eq "Unattached" | Select-Object DiskName, SizeGB, Type, ResourceGroup

NIC — Interfaces réseau

Inventorie toutes les interfaces réseau avec leur IP privée, méthode d'allocation, subnet, préfixe d'adresse, VM attachée et statut (Attached/Unattached). Résout le subnet via un join KQL avec les VNets.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_NIC {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_NIC <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.network/networkinterfaces'
            | extend nicId = tolower(id)
            | extend ipConfig = properties.ipConfigurations[0]
            | extend nsgSubnetId = tolower(tostring(ipConfig.properties.subnet.id))
            | join kind=leftouter (
                Resources
                | where type =~ 'microsoft.network/virtualnetworks'
                | mv-expand subnet = properties.subnets
                | project nsgSubnetId = tolower(tostring(subnet.id)), SubnetPrefix = tostring(subnet.properties.addressPrefix)
            ) on nsgSubnetId
            | join kind=leftouter (
                Resources
                | where type =~ 'microsoft.compute/virtualmachines'
                | mv-expand nicRef = properties.networkProfile.networkInterfaces
                | project nicId = tolower(tostring(nicRef.id)), IsPrimary = tobool(nicRef.properties.primary)
            ) on nicId
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where name contains '$Cible'" }
        $KqlQuery += @"
            | project id = nicId, NicName = name, ResourceGroup = resourceGroup, Location = location, PrivateIP = tostring(ipConfig.properties.privateIPAddress), Allocation = tostring(ipConfig.properties.privateIPAllocationMethod), IsPrimary = coalesce(IsPrimary, false), Subnet = extract(@'.*/([^/]+)', 1, nsgSubnetId), SubnetPrefix = tostring(SubnetPrefix), VMName = extract(@'.*/([^/]+)', 1, tostring(properties.virtualMachine.id)), State = iff(isnotempty(properties.virtualMachine), 'Attached', 'Unattached')
            | order by VMName asc, IsPrimary desc, NicName asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_NIC : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_NIC >>>$($PSStyle.Reset)"
    }
}
# Toutes les NICs
$nics = Get-Faitza_AZ_NIC
$nics | Format-Table NicName, VMName, PrivateIP, Subnet, State -AutoSize

# NICs non attachées (orphelines)
$nics | Where-Object State -eq "Unattached" | Select-Object NicName, ResourceGroup, Location

ResourceTags — Tags des ressources

Retourne tous les tags des ressources des types principaux (VMs, NICs, disques, NSGs, VNets, route tables) avec les clés triées alphabétiquement pour faciliter la comparaison et l'audit de gouvernance.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_ResourceTags {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_ResourceTags <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            Resources
            | where isnotempty(tags)
            | where type in~ ('microsoft.compute/virtualmachines','microsoft.network/networkinterfaces','microsoft.compute/disks','microsoft.network/networksecuritygroups','microsoft.network/virtualnetworks','microsoft.network/routetables')
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where name contains '$Cible'" }
        $KqlQuery += @"
            | project id = tolower(tostring(id)), ResourceName = tostring(name), ResourceType = tostring(type), Tags = tostring(tags)
            | order by ResourceName asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        $Reponse = Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers -CallerName "ResourceTags"

        foreach ($item in $Reponse) {
            if (-not [string]::IsNullOrWhiteSpace($item.Tags)) {
                try {
                    $obj = $item.Tags | ConvertFrom-Json -AsHashtable
                    $sorted = [ordered]@{}
                    foreach ($k in ($obj.Keys | Sort-Object)) { $sorted[$k] = $obj[$k] }
                    $item.Tags = $sorted | ConvertTo-Json -Compress
                } catch {}
            }
        }
        return $Reponse
    } catch {
        throw "Erreur Get-Faitza_AZ_ResourceTags : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_ResourceTags >>>$($PSStyle.Reset)"
    }
}
# Audit des tags de toutes les ressources
$tags = Get-Faitza_AZ_ResourceTags
$tags | Format-Table ResourceName, ResourceType, Tags -AutoSize

# Ressources sans tag "Environment" (audit gouvernance)
$tags | Where-Object { $_.Tags -notmatch '"Environment"' } | Select-Object ResourceName, ResourceType

RouteTable — Tables de routage

Inventorie toutes les routes de toutes les route tables avec leur préfixe, type de next hop et IP de next hop.

Droits requis

Reader sur les souscriptions · Token arm

function Get-Faitza_AZ_RouteTable {
    param ([string]$Cible = "", [string]$sub = "")
    try {
        Write-Host "$($PSStyle.Background.Red)>>> Get-Faitza_AZ_RouteTable <<<$($PSStyle.Reset)"
        $KqlQuery = @"
            Resources
            | where type =~ 'microsoft.network/routetables'
            | mv-expand route = properties.routes
"@
        if (-not [string]::IsNullOrWhiteSpace($Cible)) { $KqlQuery += "`n            | where name contains '$Cible'" }
        $KqlQuery += @"
            | project id = tolower(tostring(route.id)), RouteTableName = name, ResourceGroup = resourceGroup, Location = location, DisableBgpRouteProp = tobool(properties.disableBgpRoutePropagation), RouteName = tostring(route.name), AddressPrefix = tostring(route.properties.addressPrefix), NextHopType = tostring(route.properties.nextHopType), NextHopIpAddress = iff(isnotempty(route.properties.nextHopIpAddress), tostring(route.properties.nextHopIpAddress), "N/A")
            | order by RouteTableName asc, RouteName asc
"@
        $BodyHash = @{ query = $KqlQuery }
        if (-not [string]::IsNullOrWhiteSpace($sub)) { $BodyHash.subscriptions = @($sub) }
        $Headers = Get-Faitza_Modul_Headers -resource "arm"
        return Invoke-Faitza_AZ_ARG -Body ($BodyHash | ConvertTo-Json -Depth 10) -Headers $Headers
    } catch {
        throw "Erreur Get-Faitza_AZ_RouteTable : $_"
    } finally {
        Write-Host "$($PSStyle.Background.Red)<<< Get-Faitza_AZ_RouteTable >>>$($PSStyle.Reset)"
    }
}
# Toutes les routes
Get-Faitza_AZ_RouteTable | Format-Table RouteTableName, RouteName, AddressPrefix, NextHopType, NextHopIpAddress -AutoSize

# Routes vers un NVA spécifique (VirtualAppliance)
Get-Faitza_AZ_RouteTable | Where-Object NextHopType -eq "VirtualAppliance" | Select-Object RouteTableName, AddressPrefix, NextHopIpAddress

// Commentaires

Aucun commentaire pour l'instant.