{"id":49458206,"url":"https://github.com/mindaugasnakrosis/azure-costs-analyzer","last_synced_at":"2026-04-30T08:01:51.313Z","repository":{"id":354696210,"uuid":"1224758986","full_name":"mindaugasnakrosis/azure-costs-analyzer","owner":"mindaugasnakrosis","description":"Read-only Azure cost \u0026 FinOps audit, delivered as a Claude Code skill. Snapshots a tenant via az CLI, evaluates against Microsoft + FinOps Foundation rules, produces a written analysis suitable for a PE operating partner.","archived":false,"fork":false,"pushed_at":"2026-04-29T16:13:12.000Z","size":141,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-29T18:14:53.025Z","etag":null,"topics":["azure","azure-finops","claude-code","claude-skill","cloud-cost","cost-optimization","finops","pe-operations"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mindaugasnakrosis.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/contributing-a-rule.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-29T15:44:25.000Z","updated_at":"2026-04-29T16:14:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mindaugasnakrosis/azure-costs-analyzer","commit_stats":null,"previous_names":["mindaugasnakrosis/azure-costs-analyzer"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/mindaugasnakrosis/azure-costs-analyzer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindaugasnakrosis%2Fazure-costs-analyzer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindaugasnakrosis%2Fazure-costs-analyzer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindaugasnakrosis%2Fazure-costs-analyzer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindaugasnakrosis%2Fazure-costs-analyzer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mindaugasnakrosis","download_url":"https://codeload.github.com/mindaugasnakrosis/azure-costs-analyzer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mindaugasnakrosis%2Fazure-costs-analyzer/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32458237,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T22:27:22.272Z","status":"online","status_checked_at":"2026-04-30T02:00:05.929Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["azure","azure-finops","claude-code","claude-skill","cloud-cost","cost-optimization","finops","pe-operations"],"created_at":"2026-04-30T08:01:45.099Z","updated_at":"2026-04-30T08:01:51.299Z","avatar_url":"https://github.com/mindaugasnakrosis.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# azure-investigator\n\n[![tests](https://github.com/mindaugasnakrosis/azure-costs-analyzer/actions/workflows/test.yml/badge.svg)](https://github.com/mindaugasnakrosis/azure-costs-analyzer/actions/workflows/test.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/)\n[![Built with uv](https://img.shields.io/badge/built%20with-uv-DE5FE9.svg)](https://docs.astral.sh/uv/)\n\n**Read-only Azure cost \u0026 FinOps audit, delivered as a Claude Code skill.** Snapshots an Azure tenant via the `az` CLI, evaluates against published Microsoft + FinOps Foundation rules, and produces a written analysis (`report.md` + `findings.yaml`) suitable for forwarding to a portfolio CTO.\n\n\u003e Built for the **20-minute Azure cost review** of a freshly-acquired portfolio company — every finding grounded in a citable authority (Azure Well-Architected Framework, Cloud Adoption Framework, Advisor recommendation reference, FinOps Foundation, Retail Prices API).\n\n---\n\n## Who this is for\n\n- **PE operating partners and portco CTOs** running cost audits post-acquisition or pre-investment, who need a forward-able artefact in week 1, not week 4.\n- **FinOps practitioners** who want a starting point for a structured Azure cost review with citation-grounded thresholds rather than vibes.\n- **DevOps / platform engineers** who want a read-only inventory of cost waste in a tenant they've inherited.\n- **Claude Code users** who want a real-world example of a skill with a proper persona, knowledge corpus, and architectural firewall.\n\nIf you need a remediation tool, this isn't it. The skill is **read-only by architectural guarantee** (33 forbidden write verbs, enforced at the subprocess boundary, 23 unit tests). It produces investigations, not actions.\n\n---\n\n## What it produces\n\nA `report.md` that opens with headline GBP savings range, top-3 quick wins, top-3 strategic recommendations, then severity-grouped findings. Plus a flat `findings.yaml` for any downstream consumer. See [`docs/example-report.md`](docs/example-report.md) for a full sanitised sample.\n\n```\n# Azure cost review — 2026-04-29T14-41-29Z\n\n- Total estimated monthly savings: £97 – £316 / month\n- Findings by severity: Critical 0 · High 0 · Medium 12 · Low 136 · Info 60\n\n## Top 3 quick wins\n1. Orphaned managed disk: ...-containerRootVolume — £31–£38/mo (severity Medium, confidence High)\n2. Orphaned managed disk: ...-osDisk — £4/mo (severity Medium, confidence High)\n3. Unattached Standard public IP: pip-test-natgw-01 — £2–£3/mo (severity Medium, confidence High)\n```\n\nEvery finding cites the `knowledge/*.md` document grounding it. Savings figures are GBP retail-rate ceilings — reservations and negotiated discounts are explicitly not netted out. Severity (Critical → Info) and confidence (High / Medium / Low) are separate axes, both reported.\n\n---\n\n## Architectural guarantees\n\n- **Read-only is absolute.** The core `azcli.py` wrapper refuses 33 write verbs (`update`, `delete`, `create`, `set`, `add`, `remove`, `assign`, `start`, `stop`, `restart`, `deallocate`, `tag update`, `policy assignment`, …) at the subprocess boundary. Verified by 23 dedicated unit tests. Safe to run against production without a change-management window.\n- **Knowledge corpus is hardcoded, versioned, citable.** Each `knowledge/*.md` ships with frontmatter (canonical URL, retrieval date, content SHA-256, `cited_by` list) and verbatim quotes the rule it grounds. The analyser refuses to run a rule whose declared `knowledge_refs` are absent. No live web fetches at runtime.\n- **Two skills, one shared core.** `azure-cost-investigator` (FinOps persona) ships in v1; `azure-security-investigator` (security persona, same architecture) is a reserved namespace today and ships in v2. Skills never import each other.\n- **GBP currency.** Retail Prices API queried with `currencyCode=GBP`; reports format figures as £.\n- **Subscription scope.** Iterates every subscription the signed-in identity can access. `--subscription` and `--exclude` flags narrow scope.\n- **Severity ≠ confidence.** A Medium-severity orphan disk (deterministic) and a Medium-severity oversized VM (CPU-only heuristic) are reported with different confidence levels so a reviewer knows where to push back.\n\nSee [`docs/architecture.md`](docs/architecture.md) for the full rationale.\n\n---\n\n## Layout\n\n```\npackages/\n  azure-investigator-core/         # shared: auth, az wrapper, snapshot, pricing, schema, knowledge loader\n  azure-cost-investigator/         # cost / FinOps skill (v1) — 11 rules, 11-doc knowledge corpus\n  azure-security-investigator/     # reserved namespace; ships in v2 (stub today)\nscripts/\n  install_skill.sh                 # symlinks each SKILL.md into ~/.claude/skills/\n  refresh_knowledge.py             # maintainer-only: re-fetch knowledge sources, surface drift\ndocs/\n  architecture.md                  # one-page: why two skills + one core\n  contributing-a-rule.md           # how to author a new cost rule\n  example-report.md                # sanitised sample report.md output\n```\n\n---\n\n## Requirements\n\n- **Python 3.11+** (3.12 also tested in CI).\n- **[`uv`](https://docs.astral.sh/uv/)** for the Python toolchain. Install via `curl -LsSf https://astral.sh/uv/install.sh | sh` or your platform's package manager.\n- **[Azure CLI (`az`)](https://learn.microsoft.com/cli/azure/install-azure-cli)** logged into the tenant you want to analyse.\n- **[Claude Code](https://claude.com/code)** if you want to use the skill experience (the CLI works without it).\n- *(optional)* The `reservation` extension if your tenant has reservations: `az extension add --name reservation`.\n\n### Required Azure permissions\n\nThe signed-in `az` identity needs **read access** to whatever you want analysed. The minimum set:\n\n| Scope | Built-in role | What it enables |\n|---|---|---|\n| Subscription | **Reader** | All resource-graph collectors (vms, disks, public_ips, nics, snapshots, app_service_plans, app_services, sql, storage_accounts, resources, tags, advisor) |\n| Subscription | **Monitoring Reader** *(or Reader is usually enough)* | `vm_metrics`, `consumption` |\n| Reservation order / billing scope | **Reservations Reader** | `reservations` collector + utilisation merge |\n\nIf a role is missing the corresponding collector emits a structured error in `manifest.yaml` and the rules that depend on it downgrade to Info findings — the run never aborts. You can re-run after granting the role.\n\n---\n\n## Quickstart\n\n```bash\ngit clone https://github.com/mindaugasnakrosis/azure-costs-analyzer.git\ncd azure-costs-analyzer\nuv sync --all-packages\nbash scripts/install_skill.sh        # only needed if you want it as a Claude Code skill\n```\n\nThen, against your tenant:\n\n```bash\naz login\nuv run azure-investigator init       # writes ~/.config/azure-investigator/config.yaml\nuv run azure-investigator doctor     # verifies environment + corpus\nuv run azure-investigator pull       # snapshots every accessible subscription (5–15 min)\nuv run azure-cost-investigator analyse latest\n```\n\nOutputs land next to the snapshot manifest:\n\n| OS | Snapshot root |\n|---|---|\n| Linux | `~/.local/share/azure-investigator/snapshots/\u003cid\u003e/` |\n| macOS | `~/Library/Application Support/azure-investigator/snapshots/\u003cid\u003e/` |\n| Windows | `%LOCALAPPDATA%\\azure-investigator\\snapshots\\\u003cid\u003e\\` |\n\nPer snapshot:\n\n```\nmanifest.yaml       per-collector status, identity, subscriptions\nsubscriptions/\u003cid\u003e/ raw `az` payloads (one JSON per collector family)\npricing/            snapshot-time price cache (reproducible reports)\nreport.md           ← user-facing artefact\nfindings.yaml       ← machine-readable findings\n```\n\n---\n\n## Using as a Claude Code skill\n\nAfter `bash scripts/install_skill.sh`, restart Claude Code (or run `/skills`). Then trigger the skill with a natural-language prompt — Claude reads `SKILL.md`, decides this skill matches, and drives the CLIs for you.\n\nExample prompts that should trigger:\n\n- *\"Run an Azure cost review on my tenant. Use the latest snapshot.\"*\n- *\"Where is money being wasted in this Azure subscription? Walk me through the top 3 quick wins.\"*\n- *\"Do a 20-minute FinOps assessment of the production subscription. Cite the knowledge documents you're relying on.\"*\n\nThe skill will (in order): check `azure-investigator doctor` → decide whether to `pull` or reuse `latest` → run `azure-cost-investigator analyse` → narrate `report.md` to you, lifting the verbatim assumptions from each `SavingsRange` and citing the `knowledge/*.md` grounding each finding.\n\nIf you want to drive the engine directly without going through Claude Code, just use the CLI verbs above — the skill is optional.\n\n---\n\n## CLI surface\n\n```bash\n# core\nazure-investigator init\nazure-investigator doctor\nazure-investigator pull [--subscription ...] [--exclude ...] [--collector ...]\nazure-investigator snapshot ls\nazure-investigator snapshot show \u003cid|latest\u003e\nazure-investigator schema [finding|snapshot]\n\n# cost skill\nazure-cost-investigator analyse \u003cid|latest\u003e [--rule ...] [--exclude-rule ...] [--no-show]\nazure-cost-investigator report  \u003cid|latest\u003e [--format md|json] [--output PATH]\nazure-cost-investigator knowledge list\nazure-cost-investigator knowledge show \u003cfilename\u003e\nazure-cost-investigator schema [finding|report]\n\n# security skill — v2 stub\nazure-security-investigator analyse   # prints \"v2 — not implemented\", exits 2\n```\n\nNo mutating verbs. No `apply`, `remediate`, `fix`, `delete`. The naming is part of the read-only contract.\n\n---\n\n## Authorities the cost skill grounds itself in\n\n| Authority | Used by |\n|---|---|\n| [Azure Well-Architected Framework — Cost Optimization pillar](https://learn.microsoft.com/en-us/azure/well-architected/cost-optimization/principles) | Strategic narrative, dev-vs-prod SKU mismatches |\n| [Microsoft Cloud Adoption Framework — resource tagging](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-tagging) | Untagged-resources rule, governance findings |\n| [Azure Advisor — cost recommendation reference](https://learn.microsoft.com/en-us/azure/advisor/advisor-reference-cost-recommendations) | Mirroring the canonical taxonomy of cost findings |\n| [Azure Advisor — VM / VMSS shutdown + resize logic](https://learn.microsoft.com/en-us/azure/advisor/advisor-cost-recommendations) | Verbatim P95 CPU + outbound thresholds for idle / oversized VMs |\n| [FinOps Foundation framework](https://www.finops.org/framework/) | Inform / Optimize / Operate phases; reservation utilisation threshold |\n| [Azure Retail Prices REST API](https://learn.microsoft.com/en-us/rest/api/cost-management/retail-prices/azure-retail-prices) | All GBP savings figures |\n\nThe full corpus is 11 in-repo `.md` files at `packages/azure-cost-investigator/src/azure_cost_investigator/knowledge/`. List with `azure-cost-investigator knowledge list`; read individual docs with `azure-cost-investigator knowledge show \u003cfilename\u003e`.\n\n---\n\n## Cost rules implemented in v1\n\n| Rule | Severity (typical) | Confidence | Authority |\n|---|---|---|---|\n| `orphaned_disks` | Medium | High | Microsoft \"unattached disks\" + Advisor |\n| `unattached_public_ips` | Medium / High | High | Standard SKU billing + Basic SKU retirement |\n| `stopped_not_deallocated_vms` | Critical | High | Advisor + VM lifecycle |\n| `idle_vms` | Medium | Medium | Advisor P95 CPU \u003c 3% (verbatim) |\n| `oversized_vms` | Medium | Low | Advisor user-facing target P95 ≤ 40% |\n| `unused_app_service_plans` | High | High | App Service plan billing model + Advisor |\n| `old_snapshots` | Medium | High | Cool tier 90-day minimum + Advisor |\n| `underused_reservations` | Medium | Medium | FinOps Foundation 80% / 30-day |\n| `dev_skus_in_prod` | Medium | Medium | WAF Principle 2 + CAF tagging |\n| `untagged_costly_resources` | Low | High | CAF tagging schema + FinOps Inform phase |\n| `legacy_storage_redundancy` | Low / Medium | Medium | Storage redundancy + WAF cost pillar |\n\nTo add a twelfth, see [`docs/contributing-a-rule.md`](docs/contributing-a-rule.md). The discipline is *knowledge document first, then code, then tests* — and the analyser refuses to run a rule whose `knowledge_refs` are missing.\n\n---\n\n## Troubleshooting\n\n**`az login active` fails in `doctor`.** Run `az login` and confirm you can run a read like `az account show -o table`. The skill never runs `az login` for you.\n\n**`disks` collector errors with `the following arguments are required: --resource-group`.** Some Azure CLI builds reject `az disk list` without `-g`. The collector falls back to per-resource-group enumeration; if every per-RG call fails the `orphaned_disks` rule downgrades to an Info finding instead of running blind. Updating the Azure CLI usually clears it.\n\n**`reservations` collector returns \"all 'utilisation unknown'\".** The `reservation` extension may be missing or out of date. Run `az extension add --name reservation` (or `az extension update --name reservation`) and re-pull. The next snapshot will merge `avgUtilizationPercentage` from `az consumption reservation summary list` onto each reservation record.\n\n**`consumption` collector times out.** Cost Management can be slow on large subscriptions. The collector uses a 600s per-call timeout. If it still times out, narrow the pull with `--collector` to skip `consumption` for the first run; you can re-pull later for that one collector.\n\n**`analyse` errors with `KnowledgeRefMissing`.** A rule cites a knowledge document that's not in the corpus. Either the file was renamed (update the rule's `KNOWLEDGE_REFS`) or you ran from a partial install (`uv sync --all-packages` from the repo root re-establishes the corpus).\n\n**`bash scripts/install_skill.sh` reports `warning: ... exists and is not a symlink`.** A previous install left a real file at `~/.claude/skills/\u003cname\u003e/SKILL.md`. The script backs it up with a timestamped suffix and then symlinks; the warning is informational, not an error.\n\n---\n\n## Tests\n\n```bash\nuv run pytest                                              # 128 passed, 1 skipped (~1s)\n\n# optional: run the smoke test against a real snapshot\nAZURE_INVESTIGATOR_SMOKE_SNAPSHOT=/path/to/snapshot \\\n  uv run pytest packages/azure-cost-investigator/tests/test_real_snapshot_smoke.py\n```\n\nThe smoke test runs every rule against a real on-disk snapshot. It silently skips without the env var so CI / fresh checkouts stay green without a tenant.\n\nLint + format:\n\n```bash\nuv run ruff check .\nuv run ruff format --check .\n```\n\nCI runs all three on push and on PR against `main`, on Python 3.11 and 3.12.\n\n---\n\n## Roadmap\n\n- **`azure-security-investigator` (v2).** Same core, same architectural firewall, security persona. Knowledge corpus will quote Microsoft Cloud Security Benchmark and CIS Microsoft Azure Foundations Benchmark verbatim.\n- **Outbound-network metric collection** for VMs. Would lift `idle_vms` confidence from Medium to High by completing Microsoft's full Advisor shutdown criterion.\n- **`PricingClient` wired into rule output** for per-finding Retail Prices API lookups (currently uses packaged GBP/instance bands as ceilings).\n- **FinOps Foundation `/framework/phases/`** verbatim quotes (currently a `TODO: refetch` block in `knowledge/finops-framework.md`).\n- **Resource-graph-based pull mode** as a faster alternative for whole-tenant snapshots (currently per-RG enumeration for disks).\n\nIf you have an authority, a metric, or a rule you'd want grounded — open an issue. The pattern of \"verbatim quote → citing rule → testable threshold\" is reusable for anything with published thresholds.\n\n---\n\n## Contributing\n\nSee [`docs/contributing-a-rule.md`](docs/contributing-a-rule.md) for adding a new cost rule. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for the broader contribution flow (issues, PRs, code review).\n\nIf you find a security issue (especially anything that could let the read-only firewall be bypassed), see [`SECURITY.md`](SECURITY.md) for responsible-disclosure instructions.\n\n---\n\n## License\n\nMIT — see [`LICENSE`](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindaugasnakrosis%2Fazure-costs-analyzer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmindaugasnakrosis%2Fazure-costs-analyzer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmindaugasnakrosis%2Fazure-costs-analyzer/lists"}