Skip to content

GitHub Actions

AzureScout ships a composite action, so a workflow can produce an inventory report without installing PowerShell modules by hand or checking anything out.

yaml
- uses: Hybrid-Solutions-Cloud/azure-scout@v2
  with:
    tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
    client-id:     ${{ secrets.AZURE_CLIENT_ID }}
    client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}

The action installs the module and its dependencies, authenticates with the service principal, runs the collection, and uploads the reports as a workflow artifact.

Prerequisites

An Entra app registration (service principal) with a client secret, holding at least Reader on the scope you want to inventory. Store three values as repository secrets under Settings → Secrets and variables → Actions:

SecretValue
AZURE_TENANT_IDDirectory (tenant) ID
AZURE_CLIENT_IDApplication (client) ID
AZURE_CLIENT_SECRETClient secret value

Add AZURE_SUBSCRIPTION_ID too if you want to scan one subscription rather than every subscription the principal can read.

For scope: All or scope: EntraOnly, the app registration also needs Microsoft Graph application permissions with admin consent granted. See Permissions.

A scheduled weekly inventory

yaml
name: Weekly Azure Inventory

on:
  schedule:
    - cron: '0 6 * * 1'   # Mondays, 06:00 UTC
  workflow_dispatch:

permissions:
  contents: read

jobs:
  inventory:
    runs-on: ubuntu-latest
    steps:
      - uses: Hybrid-Solutions-Cloud/azure-scout@v2
        with:
          tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
          client-id:     ${{ secrets.AZURE_CLIENT_ID }}
          client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
          scope:         ArmOnly
          output-format: All
          artifact-retention-days: '90'

Inputs

InputDefaultDescription
tenant-id(required)Entra tenant (directory) ID
client-id''Service principal client ID
client-secret''Client secret — always from a secret, never a literal
subscription-id''Single subscription to scan; blank scans all readable subscriptions
scopeArmOnlyArmOnly, EntraOnly, or All
category''Comma-separated category filter, e.g. Compute,Networking
output-formatAllAll, React, Json, or JsonEvidence; All selects the three live formats
report-nameAzureScoutReport file name prefix
report-dirazure-scout-reportsOutput directory, relative to the workspace
module-version''Pin a PSGallery version; blank installs the latest
litetrueLegacy compatibility input for the held Excel renderer; no effect on live React/JSON outputs
include-costsfalseCollect cost data. Needs Reader plus the EA "AO view charges" / MCA "Azure charges" billing setting — not Cost Management Reader, which is redundant against Reader and cannot unlock the billing gate
upload-artifacttrueUpload the report directory as an artifact
artifact-nameazure-scout-reportsArtifact name
artifact-retention-days30Artifact retention

client-id and client-secret must be supplied together. Omit both to reuse an Azure context established by an earlier step, such as an OIDC login via azure/login.

Outputs

OutputDescription
report-dirAbsolute path to the generated report directory
report-countNumber of report files generated
yaml
- uses: Hybrid-Solutions-Cloud/azure-scout@v2
  id: scout
  with:
    tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
    client-id:     ${{ secrets.AZURE_CLIENT_ID }}
    client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}

- name: Fail if the scan produced nothing
  if: steps.scout.outputs.report-count == '0'
  run: exit 1

lite is retained for compatibility

The Excel renderer is on hold, so lite does not affect a live React/JSON output. Its retained implementation no longer uses Excel COM; internal compatibility tests run through EPPlus/ImportExcel (Build-AZSCExcelChartStyle) without an installed Excel application.

Narrowing a scan by category

A full tenant scan is expensive. Split it or narrow it:

yaml
      - uses: Hybrid-Solutions-Cloud/azure-scout@v2
        with:
          tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
          client-id:     ${{ secrets.AZURE_CLIENT_ID }}
          client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
          category:      'Compute,Networking,Storage'

Category values and their aliases are in the Category Reference.

Running the whole tenant in parallel

Matrix the categories to stay inside job time limits and get results sooner:

yaml
jobs:
  inventory:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        category:
          - 'Compute,Containers,Web'
          - 'Networking,Storage,Databases'
          - 'AI,Analytics,IoT'
          - 'Identity,Security,Management'
          - 'Hybrid,Monitor,Integration'
    steps:
      - uses: Hybrid-Solutions-Cloud/azure-scout@v2
        with:
          tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
          client-id:     ${{ secrets.AZURE_CLIENT_ID }}
          client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
          category:      ${{ matrix.category }}
          artifact-name: inventory-${{ strategy.job-index }}

Give each matrix leg a distinct artifact-name; identical names collide on upload.

Publishing reports to Azure Storage

The action leaves reports in the workspace, so any upload step can pick them up:

yaml
      - uses: Hybrid-Solutions-Cloud/azure-scout@v2
        id: scout
        with:
          tenant-id:     ${{ secrets.AZURE_TENANT_ID }}
          client-id:     ${{ secrets.AZURE_CLIENT_ID }}
          client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}

      - name: Upload to blob storage
        shell: pwsh
        env:
          TENANT_ID:     ${{ secrets.AZURE_TENANT_ID }}
          CLIENT_ID:     ${{ secrets.AZURE_CLIENT_ID }}
          CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
          REPORT_DIR:    ${{ steps.scout.outputs.report-dir }}
        run: |
          $sec  = ConvertTo-SecureString $env:CLIENT_SECRET -AsPlainText -Force
          $cred = [pscredential]::new($env:CLIENT_ID, $sec)
          Connect-AzAccount -ServicePrincipal -Tenant $env:TENANT_ID -Credential $cred | Out-Null
          $ctx = New-AzStorageContext -StorageAccountName 'yourstorageaccount' -UseConnectedAccount
          Get-ChildItem -Path $env:REPORT_DIR -File | ForEach-Object {
              Set-AzStorageBlobContent -File $_.FullName -Container 'azurescout-reports' -Blob $_.Name -Context $ctx -Force
          }

The service principal needs Storage Blob Data Contributor on the storage account — Contributor alone grants management-plane rights and still fails the data-plane write.

Security notes

  • Pass secrets through with: from secrets.* only. A literal in the workflow file is committed to history.
  • The action reads every input through an environment variable rather than interpolating $ into a script body, so a crafted input value cannot break out and execute.
  • AzureScout is read-only against your tenant. It issues no write operations.
  • permissions: contents: read is enough; the action needs no write scope on the repo.

Troubleshooting

SymptomCauseFix
Chart step fails on a hosted runnerlite: falseSet lite: true
AADSTS7000215: Invalid client secretSecret expired or mis-copiedRegenerate and update the repository secret
Zero resources collectedNo Reader on the target scopeAssign Reader to the app registration
Entra worksheets empty with scope: AllGraph application permissions missing or unconsentedSee Permissions
Artifact upload finds no filesCollection produced nothing — check the log for the resource countConfirm the scope and subscription ID
Two matrix legs overwrite each other's artifactShared artifact-nameGive each leg a distinct name

Released under the MIT License.