Skip to main content

Lightweight Azure tenant discovery and visualization via Resource Graph. Enumerates subscriptions and resources, normalizes results, and renders interactive dependency graphs. Supports public and sovereign clouds (Gov, China, Germany, Azure Stack).

Project description

Azure Discovery

Azure Discovery is a lightweight Azure tenant mapper that enumerates subscriptions and resources via Azure Resource Graph, normalizes the results, and renders an interactive dependency graph. The tool exposes both a Typer-based CLI (azure-discovery) and a FastAPI surface so the same discovery workflow can be automated or embedded in other services.

The package is published on PyPI as azure-discovery and can be installed with pip install azure-discovery.

Core capabilities

  • Builds environment-aware credential chains (Azure CLI + DefaultAzureCredential) with guardrails for unsupported clouds.
  • Queries Azure Resource Graph with include/exclude filters, tag constraints, and resource group scopes.
  • Resolves subscriptions automatically when not provided and de-duplicates resources for consistent graph IDs.
  • Produces JSON summaries, console metrics, and PyVis HTML graphs for quick triage.
  • Offers identical request/response contracts (Pydantic models) across CLI and API, following the Receive an Object, Return an Object (RORO) pattern.
  • Supports all Azure clouds: public, Government (GCC/GCC-H), China, Germany, and Azure Stack.

Installation

From PyPI (recommended):

pip install azure-discovery

With optional development dependencies:

pip install azure-discovery[dev]

From source (e.g. for development or when embedded in another repo):

git clone https://github.com/maravedi/AzureDiscovery.git
cd AzureDiscovery
pip install -e .[dev]

Package layout

When installed, the package provides the azure_discovery Python package:

azure_discovery/
  __init__.py       # run_discovery, AzureDiscoveryRequest, AzureDiscoveryResponse, etc.
  cli.py            # Typer command surface (entry point: azure-discovery)
  api.py            # FastAPI app for /discover and visualization endpoints
  orchestrator.py   # Async coordinator for enumeration + visualization
  adt_types/        # Pydantic models and custom exceptions
  enumerators/      # Resource Graph query builder and normalization
  reporting/        # Console logging and HTML/PyVis graph generation
  utils/            # Azure SDK clients, graph helpers, structured logging

Programmatic usage:

from azure_discovery import run_discovery
from azure_discovery.adt_types import (
    AzureDiscoveryRequest,
    AzureDiscoveryResponse,
    AzureEnvironment,
    DiscoveryFilter,
)

request = AzureDiscoveryRequest(
    tenant_id="<tenant-guid>",
    environment=AzureEnvironment.AZURE_GOV,
    subscriptions=["<sub-id>"],
    filter=DiscoveryFilter(include_types=["Microsoft.Compute/virtualMachines"]),
)
response = await run_discovery(request)
# response.nodes, response.relationships, response.html_report_path

To pass your own credential (e.g. for sovereign cloud from another app):

from azure_discovery.enumerators.azure_resources import enumerate_azure_resources

response = await enumerate_azure_resources(request, credential=my_credential)

Prerequisites

  • Python 3.11+
  • Azure CLI 2.60+ (optional, used when --prefer-cli is set) or service principal credentials exported as AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET
  • Azure Resource Graph access (Reader or above on the subscriptions you plan to scan)
  • Network egress to management.azure.com and api.azure.com (or the sovereign cloud endpoints you select)

Required Azure permissions

Capability Minimum RBAC role Scope recommendation
Run Resource Graph queries Reader, Resource Graph Reader, or any custom role with Microsoft.ResourceGraph/*/read Every subscription you plan to inventory or the parent management group
Auto-discover subscriptions (when --subscription is omitted) Reader on the management group or Directory.Read.All consent for service principals Tenant root (/providers/Microsoft.Management/managementGroups/<root>)
Register Microsoft.ResourceGraph (one-time) Contributor or Owner Each subscription being scanned

The tool never mutates resources, but it cannot enumerate subscriptions or call Resource Graph unless the identity has at least Reader at the relevant scope. Grant the narrowest scope that still covers your target estate (for example, a dedicated management group for security tooling).

Service principal flow (CLI based)

az ad sp create-for-rbac \
  --name azure-discovery-sp \
  --role "Reader" \
  --scopes /subscriptions/<sub-id-1> /subscriptions/<sub-id-2> \
  --years 1

az role assignment create \
  --assignee <appId> \
  --role "Resource Graph Reader" \
  --scope /subscriptions/<sub-id-1>

Export the emitted appId, tenant, and password as AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET. Repeat the role assignment command for every subscription or assign at the management group scope (/providers/Microsoft.Management/managementGroups/<mg-id>) to cover multiple subscriptions at once.

User-assigned permissions (Portal)

  1. Open Azure Portal → Subscriptions → select each target subscription.
  2. Navigate to Access control (IAM)AddAdd role assignment.
  3. Pick the Reader (or Resource Graph Reader) role, then select the user or managed identity that will run AzureDiscovery.
  4. If you want automatic subscription discovery, repeat the assignment at the tenant root management group (visible under Management groups). Users need the Azure RBAC Reader role there.

Provider registration and validation

Run the following once per subscription to ensure the Resource Graph service is registered and the identity can query it:

az account set --subscription <sub-id>
az provider register --namespace Microsoft.ResourceGraph
az graph query -q "Resources | take 1"

Successful output from az graph query confirms both the provider registration and the assigned role. If the command fails with AuthorizationFailed, double-check the scope of the role assignments and replicate them for every subscription you intend to scan.

Getting started

  1. Install (see Installation above).

  2. Authenticate to Azure

    • Interactive: az login --tenant <tenant-id> and, if multiple tenants, az account set --subscription <sub-id>.
    • Service principal: export AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET, and (optionally) AZURE_SUBSCRIPTION_ID.
    • For sovereign clouds (e.g. Azure Government): set az cloud set --name AzureUSGovernment and log in; or use a service principal with the appropriate authority.
  3. Run the CLI

    After install, the azure-discovery console script is on your PATH:

    azure-discovery discover \
      --tenant-id <tenant-guid> \
      --subscription <sub-id-1> --subscription <sub-id-2> \
      --include-type "Microsoft.Compute/virtualMachines" \
      --resource-group core-infra \
      --required-tag environment=prod \
      --visualization-output-dir artifacts/graphs
    

    Run as a module from source (from the repo root):

    python -m azure_discovery.cli discover --help
    python -m azure_discovery.cli discover --tenant-id <tenant-guid> --environment azure_gov [options...]
    

    The command prints the structured AzureDiscoveryResponse JSON and writes an interactive HTML graph under artifacts/graphs/.

  4. Run the API (optional)

    uvicorn azure_discovery.api:app --host 0.0.0.0 --port 8000 --reload
    
    • Health check: curl http://localhost:8000/healthz
    • Discovery: curl -X POST http://localhost:8000/discover -H "Content-Type: application/json" -d '{"tenant_id": "...", ... }'
    • Download visualization: curl http://localhost:8000/visuals/<file-name> --output graph.html

Configuration reference

Option Description
--tenant-id Required Entra ID tenant GUID.
--environment Azure cloud (azure_public, azure_gov, azure_china, azure_germany, azure_stack).
--subscription/-s Repeatable flag to scope runs to explicit subscription IDs. Omit to auto-resolve.
--include-type / --exclude-type Filter resource types (case-insensitive).
--resource-group Restrict discovery to named resource groups.
--required-tag Enforce tag key=value pairs (repeatable).
--prefer-cli Place Azure CLI credentials at the front of the chain.
--visualization-output-dir Directory for PyVis HTML output (default artifacts/graphs).
--visualization-file Override the generated HTML file name.
--output/-o Write JSON output to file instead of stdout.
--quiet/-q Suppress all logs except errors.
--format/-f Output format: json (default) or json-compact.

Programmatic workflows can instantiate AzureDiscoveryRequest directly and call orchestrator.run_discovery, receiving an AzureDiscoveryResponse that contains resolved subscriptions, normalized nodes, inferred relationships, and an optional html_report_path.

Output and logging separation

By default, the CLI writes JSON results to stdout and logs to stderr. This allows clean piping:

# Pipe JSON output to jq for filtering
azure-discovery discover --tenant-id <id> | jq '.discovered_subscriptions'

# Write output to file and suppress logs
azure-discovery discover --tenant-id <id> --output results.json --quiet

# Compact JSON output for scripting
azure-discovery discover --tenant-id <id> --format json-compact

Development

Quick start

# Install with development dependencies
make install-dev

# Run tests
make test

# Format code
make format

# Run linting
make lint

# Type checking
make typecheck

# Generate coverage report
make coverage

Available make commands

Run make help to see all available commands:

  • make install - Install package dependencies
  • make install-dev - Install with development dependencies
  • make test - Run tests with pytest
  • make lint - Run ruff linter
  • make format - Format code with ruff
  • make typecheck - Run mypy type checking
  • make coverage - Generate test coverage report
  • make clean - Remove build artifacts and cache
  • make run-api - Run FastAPI server locally

Pre-commit hooks

Install pre-commit hooks to automatically run linting and formatting on commit:

pip install pre-commit
pre-commit install

This will run ruff formatting, linting, and mypy type checking before each commit.

Environment variables

Copy .env.example to .env and configure your Azure credentials:

cp .env.example .env
# Edit .env with your credentials

For detailed contributing guidelines, see CONTRIBUTING.md.

Troubleshooting

  • AzureClientError: Unable to enumerate subscriptions – ensure the identity has at least Reader on one subscription and that the Resource Graph service is registered (az provider register --namespace Microsoft.ResourceGraph).
  • Resource Graph query failure – check that the tenant/subscription pair belongs to the same cloud you selected, and verify network egress to the relevant resource_manager endpoint (see _ENVIRONMENT_MAP in azure_discovery.utils.azure_clients).
  • VisualizationError: Failed to render HTML graph – confirm the --visualization-output-dir path exists and is writable; the PyVis writer does not auto-create directories unless it has permissions on each parent.
  • 401 or interaction_required errors – when running non-interactively, use a service principal credential chain and set AZURE_CLIENT_SECRET; the default chain will otherwise attempt to launch an interactive browser flow.
  • Empty graph output – verify filters are not mutually exclusive (e.g., mixing include/exclude for the same type) and that --max-batch-size in the request (via API) is not set below the minimum (100).

Known gaps and future opportunities

  • Identity & RBAC coverage – the tool currently inventories Azure Resource Manager assets only; Entra ID objects, role assignments, and policy definitions are not ingested or visualized.
  • Change tracking – runs are stateless; there is no persistence layer or diffing between historical discoveries.
  • Security context – vulnerability, compliance, and workload insights (e.g., Defender for Cloud signals) are not integrated.
  • API hardening – the FastAPI surface is unprotected (no authN/Z, rate limiting, or request quotas); deploy behind an API gateway or add middleware before production use.
  • Scale controls – back-off and throttling are minimal (throttle_delay_seconds only); there is no adaptive rate control or batching heuristics for very large tenants.

These items are valuable next steps if you intend to operationalize Azure Discovery beyond ad-hoc investigations.

Publishing to PyPI (maintainers)

To publish a new version to PyPI:

  1. Bump version in pyproject.toml.
  2. Ensure tests pass: pip install -e .[dev] && pytest.
  3. Build: python -m build.
  4. Upload: twine upload dist/azure-discovery-<version>* (requires PyPI credentials or token).

The package uses a single top-level package azure_discovery to avoid namespace conflicts on install. The console script azure-discovery is provided by the [project.scripts] entry in pyproject.toml.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

azure_discovery-0.1.2.tar.gz (26.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

azure_discovery-0.1.2-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

Details for the file azure_discovery-0.1.2.tar.gz.

File metadata

  • Download URL: azure_discovery-0.1.2.tar.gz
  • Upload date:
  • Size: 26.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.10

File hashes

Hashes for azure_discovery-0.1.2.tar.gz
Algorithm Hash digest
SHA256 e1483b3bfe98a0ead247bf3e37c57c6c4a45d8c13a30b997802a339dcec2daf0
MD5 b42e2ed8fba71ce48626859190132ab0
BLAKE2b-256 aea4d312cd7a33fac107f11d65f21b00ef7b8ad6c920f119762ca7a834d53a96

See more details on using hashes here.

File details

Details for the file azure_discovery-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for azure_discovery-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 10292b07d772d3ea2ccebda41f6f6e5b63b36e37c0a1782fb2b4eef834d103c6
MD5 c8544553454efc775086f3e9103a9f79
BLAKE2b-256 5a3c85de79a5868a7243d9622346a1481186e17978cf079cc73f9eab7f808ed3

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page