[![CI](https://github.com/your-org/azure-policy-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/your-org/azure-policy-engine/actions/workflows/ci.yml)

# Azure Policy Engine (minimal)

This repository contains a small Python package that helps author, store, and (optionally) deploy Azure Policy artifacts.

This README documents the current feature set, CLI usage, packaging/dev setup, examples and a short HOWTO for mapping JSON into SDK models.

## Overview

Supported backends

- file: author and manage policy artifacts as local JSON files. The default folder structure (under the configured `policy_dir`, defaults to the package `policies/` folder) is:
  - `definitions/` — policyDefinition JSON files
  - `assignments/` — policyAssignment JSON files
  - `initiatives/` — policySetDefinition (initiative) JSON files

- sdk: when the Azure management SDKs are available the engine uses `azure.mgmt.resource.PolicyClient` and related models to perform operations. If the SDK is not present (or an SDK call fails and a credential is available) the engine falls back to ARM REST calls using an ARM token acquired from the provided credential.

- azcli: when the Azure CLI (`az`) is installed and the user is authenticated the engine can invoke `az` to perform policy operations. This backend uses `az rest` under the hood to call ARM endpoints and is useful when you prefer CLI tooling or when SDK/REST usage is constrained. It requires `az` installed and an authenticated session (e.g., `az login`).

Key features

- Create/list/get policy definitions and initiatives (policy sets).
- Create/update/delete/list policy assignments.
- Assign initiatives (policy sets) via assignment resources.
- Scope validation and support for different assignment scopes: subscription, resource-group and management-group levels.
- Dry-run mode that prints the prepared payload and effective scope without making API calls.
- SDK model construction: the engine attempts to construct SDK model objects (PolicyAssignment, PolicyDefinition, PolicySetDefinition) when the SDK is available; otherwise it works with raw dicts.

## CLI quick reference

The CLI entry point is `python -m azure_policy_engine.cli`.

Common flags

- `--backend` (file|sdk|azcli) — choose local file backend, SDK-backed operations, or Azure CLI-backed operations (default: file).
- `--policies-dir` — override the policies directory (defaults to package `policies/`).
- `--subscription-id` — subscription id used by SDK/ARM REST calls when a scope isn't explicitly provided.
- `--scope` — provide an explicit scope for assignment operations (must start with `/`).
- `--mgmt-group` — convenience shorthand; builds the management group scope `/providers/Microsoft.Management/managementGroups/{id}`.
- `--dry-run` — print the prepared payload and scope, do not perform the SDK/REST call.

Commands (high level)

- `list` — list local or SDK policy definitions
- `get <name>` — get a single policy definition
- `deploy <name> <file>` — create/update a policy definition
- `assignments` — list assignments
- `create-assignment <name> <file>` — create an assignment (supports `--dry-run` and `--scope` / `--mgmt-group`)
- `update-assignment <name> <file>`
- `delete-assignment <name>`
- `add-assignment <name> <file>` — write file directly into assignments/ (file backend only)
- `list-initiatives` — list initiatives (policy sets)
- `assign-initiative <initiative-name> <assignment-name> <file>` — create an assignment that references an initiative

Examples

Dry-run creating an assignment at a management group (shorthand) using `azcli` backend:

```bash
python -m azure_policy_engine.cli --backend azcli --mgmt-group my-mg --dry-run create-assignment myAssign examples/assign-mg.json
```

Create an assignment at a subscription scope using the SDK backend:

```bash
python -m azure_policy_engine.cli --backend sdk --scope /subscriptions/0000 create-assignment myAssign examples/assign.json
```

Create an assignment at a subscription scope using the Azure CLI backend:

```bash
python -m azure_policy_engine.cli --backend azcli --scope /subscriptions/0000 create-assignment myAssign examples/assign.json
```

Assign an initiative (dry-run):

```bash
python -m azure_policy_engine.cli --backend sdk --mgmt-group my-mg --dry-run assign-initiative myInitiative myAssign examples/assign-mg.json
```

## Scope validation and supported forms

The engine implements a pragmatic scope validator to catch common user mistakes. Accepted forms include:

- subscription root: `/subscriptions/{subscriptionId}`
- resource group: `/subscriptions/{subscriptionId}/resourceGroups/{rg}`
- management group: `/providers/Microsoft.Management/managementGroups/{mgId}`

If you pass an explicit scope with `--scope`, it must start with `/`. Use `--mgmt-group` as a convenience shorthand to build management-group scopes.

## Dry-run details

`--dry-run` is available for create/update/delete assignment and assign-initiative commands. When used the CLI prints a JSON object that contains:

- `dry_run`: true
- `action`: the intended action
- `scope`: effective normalized scope
- `name`: the assignment name
- `payload`: the assignment body or SDK-model-as-dict (if SDK model was constructed)

This helps you validate payload shape and scope before making changes.

## Examples folder

An `examples/` folder is provided with a sample management-group assignment JSON and a small helper script:

- `examples/assign-mg.json` — example assignment JSON that references an initiative at management-group scope.
- `examples/run-assign-mg.sh` — example script that ensures `AZURE_SUBSCRIPTION_ID` is set, runs `az login` interactively if no SP credentials are present, and executes a dry-run create-assignment against `my-mg`.

Make the script executable locally and run it:

```bash
chmod +x examples/run-assign-mg.sh
bash examples/run-assign-mg.sh
```

## Packaging, pinned SDKs and dev setup

This project includes minimal packaging and pinned dependencies to make local development and CI reproducible:

- `pyproject.toml` — basic project metadata and pinned runtime dependencies used by the analyzer/CI.
- `requirements.txt` — pinned versions for local `pip install -r requirements.txt` convenience.
- `tox.ini` — tiny tox configuration to run tests in an isolated environment.

Pinned SDKs used here (change if you need different versions):

- azure-identity==1.12.0
- azure-mgmt-resource==23.0.0
- azure-mgmt-policyinsights==1.0.0
- requests==2.31.0

Dev/test

Install editable package + dev deps in a venv:

```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e .
pip install -r requirements.txt
```

Run tests:

```bash
PYTHONPATH=. pytest -q
```

or with tox:

```bash
tox
```

## CI

A GitHub Actions workflow is added at `.github/workflows/ci.yml` that:

- Checks out the repository
- Sets up Python
- Installs the package in editable mode and the pinned requirements
- Runs the test suite with pytest

You can extend the workflow (matrix Python versions, caching, linting) as needed.

## HOWTO: map a policy JSON into SDK model fields

When deploying via the SDK it's more robust to construct SDK model objects and pass them to the client methods.

- Ensure `properties.policyDefinitionId` contains the full resource id of the definition or initiative.
- Construct the SDK model (PolicyAssignment / PolicyDefinition / PolicySetDefinition) using the JSON `properties` as kwargs where possible.
- Use `model.as_dict()` to verify the final payload shape.
- The engine contains helper methods (`_make_policy_assignment_model`, `_make_policy_definition_model`, `_make_policy_set_definition_model`) that attempt to construct SDK models when the SDK is available and fall back to dicts otherwise.

## Troubleshooting

- If `--backend sdk` fails due to missing credentials, provide a `DefaultAzureCredential` compatible credential (e.g., `az login` for interactive flows, or set `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_CLIENT_SECRET` for a service principal).
- If using `--backend azcli` ensure `az` is installed and authenticated:
  - Install: follow https://aka.ms/azcli
  - Authenticate: `az login` or `az login --service-principal -u <id> -p <secret> --tenant <tenant>`

## Authentication via environment variables

The engine supports authenticating using Azure environment variables. You can either provide a service principal (recommended for CI/non-interactive flows) or rely on DefaultAzureCredential (interactive `az login`, managed identity, etc.).

Service principal (export into your shell):

```bash
export AZURE_CLIENT_ID="<YOUR_CLIENT_ID_HERE>"
export AZURE_CLIENT_SECRET="<YOUR_CLIENT_SECRET_HERE>"
export AZURE_TENANT_ID="<YOUR_TENANT_ID_HERE>"
export AZURE_SUBSCRIPTION_ID="<YOUR_SUBSCRIPTION_ID_HERE>"
```

Quick dry-run example (SDK backend)

```bash
# with SP creds exported as above
python -m azure_policy_engine.cli --backend sdk --mgmt-group my-mg --dry-run create-assignment myAssign examples/assign-mg.json
```

Default credential (interactive / managed identity):

- For local interactive auth, run:

```bash
az login
export AZURE_SUBSCRIPTION_ID="<YOUR_SUBSCRIPTION_ID_HERE>"
```

How the code uses these variables:

- If `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, and `AZURE_TENANT_ID` are present, the engine will create an `azure.identity.ClientSecretCredential` using those values.
- Otherwise the engine falls back to `azure.identity.DefaultAzureCredential` (so `az login` or a managed identity will work).
- `AZURE_SUBSCRIPTION_ID` is used when the SDK/backends need a subscription context.

Quick try-it commands (zsh/bash):

```bash
# install dependencies into a venv
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# export SP creds and run the CLI using SDK backend
export AZURE_CLIENT_ID="<ID>"
export AZURE_CLIENT_SECRET="<SECRET>"
export AZURE_TENANT_ID="<TENANT>"
export AZURE_SUBSCRIPTION_ID="<SUB>"
python -m azure_policy_engine.cli --backend sdk list
```
