# 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](https://pypi.org/project/azure-discovery/) 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):**

```bash
pip install azure-discovery
```

**With optional development dependencies:**

```bash
pip install azure-discovery[dev]
```

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

```bash
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:**

```python
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):

```python
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)** → **Add** → **Add 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](#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:

   ```bash
   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):

   ```bash
   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)**

   ```bash
   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:

```bash
# 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

```bash
# 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:

```bash
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:

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

For detailed contributing guidelines, see [CONTRIBUTING.md](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`.