Add Start-FinOpsMultitool cmdlet — interactive GUI for tenant-wide FinOps scanning#2155
Add Start-FinOpsMultitool cmdlet — interactive GUI for tenant-wide FinOps scanning#2155z-larsen wants to merge 65 commits into
Conversation
… GUI Adds the Azure FinOps Multitool as a new PowerShell cmdlet in the FinOps toolkit. The Multitool is a WPF-based GUI that scans an Azure tenant for cost optimization, governance, and FinOps insights including cost trends, orphaned resources, idle VMs, tag hygiene, reservation/savings plan utilization, AHB opportunities, budgets, anomaly alerts, and policy compliance. - Public/Start-FinOpsMultitool.ps1: thin launcher cmdlet with comment-based help - Private/FinOpsMultitool/: full implementation (24 scanner modules, WPF GUI, Power BI template) - Tests/Unit/Start-FinOpsMultitool.Tests.ps1: Pester unit tests Windows-only (requires WPF support).
|
@microsoft-github-policy-service agree company="Microsoft" |
|
@z-larsen This is exciting! I don't know much about the tool, but would love to learn more. Can you join us at the contributor sync next Wednesday to share? |
|
Thanks, Michael! Would love to join. |
…info - Add contract-aware cost access warning banner (EA/MCA/CSP) on Overview tab - Add contract-specific billing tab messages when billing access unavailable - Add MG hierarchy unavailable info node in tree view with role guidance - Fix tag cost queries: use TagKey grouping type (not Tag/Dimension) - Add batched TagKey+TagValue query attempt with per-tag fallback - Clear skipSubs between batched and per-tag strategies - Add throttle pacing (2s every 2 queries) to avoid 429s - Add EA/MCA cost access detection in Get-CostData - Add runspace pool for API call parallelization
MSBrett
left a comment
There was a problem hiding this comment.
Review: FinOps Multitool (TUI / CLI / MCP)
Thanks for this — it's an ambitious, genuinely useful contribution and the breadth of capabilities is impressive. I ran hands-on UAT on the three core surfaces and did a code + security pass with a couple of reviewers. Good news up front: the TUI, CLI, and MCP server all run. Module imports cleanly (352 commands), the TUI parses and launches with 0 errors, and the MCP server starts and responds to tools/list / tools/call. Since TUI/CLI/MCP are the features that matter for this release, the WPF/GUI piece can be dropped without blocking — but the current entry point still points at it, which is the first must-fix below.
I'm marking this as comment rather than request-changes since it's a draft, but there are a few hard blockers I'd want resolved before merge. Grouped by severity.
🔴 Blockers
1. Public entry point is dead on arrival (Public/Start-FinOpsMultitool.ps1).
Line 45 builds a path to Private/FinOpsMultitool/Start-FinOpsMultitool.ps1 and invokes it (line 54), but that file doesn't exist — there's no gui/ and no MainWindow.xaml. The Test-Path guard at line 47 means the public cmdlet emits "FinOps Multitool files not found..." and returns. Only the TUI (Private/.../Invoke-FinOpsMultitool.ps1) actually exists. Since WPF is cut, please repoint the public cmdlet at the TUI and update its help (lines 9, 19 still describe a "WPF-based GUI"). Right now the only public command is non-functional.
2. The "read-only" contract is not true — there are 4 live Azure write tools.
The PR description and SKILL.md state all tools are read-only / Reader-scope, but these perform real ARM mutations via Invoke-AzRestMethodWithRetry:
Remove-OrphanedResource.ps1:236— DELETEStop-IdleVm.ps1:130— deallocate POSTEnable-HybridBenefit.ps1:151— PATCHSet-CostAllocationRule.ps1:238— PUT
All four are advertised unfiltered over tools/list. Also worth noting: the server exposes 40 tools, not the 21/22 claimed in the docs and tests. The read-only framing needs to be corrected everywhere (PR description, SKILL.md lines 5/13/15/133), or these tools need to be gated/opt-in.
3. Set-CostAllocationRule bypasses the write-safety gate entirely. (most material issue)
Every other write tool routes through Resolve-WriteDecision (e.g. Remove-OrphanedResource.ps1:188), which enforces ReadOnly/Enforced mode, the confirmation token, protected-tag/sub/RG guardrails, impact caps, and the audit log. Set-CostAllocationRule has no ConfirmationToken param and never calls Resolve-WriteDecision — it rolls its own -Apply dry-run (line 217) then PUTs directly (line 238). Consequences I verified:
- Setting
FINOPS_WRITE_MODE=ReadOnlydoes not stop this tool (ReadOnly is only enforced insideResolve-WriteDecision). An operator who believes they're in read-only mode can still have chargeback rules written. - These writes are never audited — no
applyentry hits the audit log, so the "every apply is logged" guarantee is false for cost-allocation writes.
Please route this tool through the same gate as the other three.
4. String JSON-RPC id terminates the server (Start-McpServer.ps1).
All handlers and Send-Result / Send-Error type the id as [int] (lines 1530–1626), and the dispatch switch (line 1684) sits outside the inner try/catch — the outer try (1664) has a finally but no catch. A spec-legal string id ("abc") throws an argument-transformation error that abandons the while($true) loop and exits the process. Many MCP clients use string ids, so this will hard-crash the server in practice. Suggest typing ids as [object]/[string] and echoing them back verbatim.
🟠 High
5. Default write mode is Interactive, which one-shots writes without a token.
Confirm-WriteAction.ps1:51-52 defaults the mode to Interactive; the Apply path only requires a confirmation token when Mode -eq 'Enforced' (line 259). .vscode/mcp.json sets no FINOPS_WRITE_MODE, so the default applies — an agent can perform an irreversible delete with a single apply=true call. To be fair: dry-run is the genuine default (apply must be explicitly set) and the protected-tag list + orphan-type allowlist still apply, so this is partly by-design "low friction." But I'd strongly recommend defaulting to ReadOnly (or requiring a token in Interactive too), especially for an agent-driven surface.
6. SKILL.md misrepresents the safety contract to the agent.
Lines 5/13/15/133 tell the LLM all tools are read-only and "never modify resources." Because this is the agent-facing contract an LLM uses to decide an action is safe, the misstatement is higher-risk than a normal doc bug — please fix alongside #2.
🟡 Medium
7. run_full_scan invokes remediation tools.
Invoke-FullScan (Start-McpServer.ps1:1373) selects every tool except _full_scan with no category filter, so remediation tools (and set_cost_allocation_rule) get called in the loop. To be precise on impact: missing mandatory params raise ParameterBindingException which is caught at line 1500 and recorded in $errors — so the scan does not crash or mutate anything (Apply is never passed). The real effect is noisy per-tool errors / junk in results. Please filter the scan to read-only/diagnostic tools by category.
8. nuget.exe is downloaded from a mutable URL and executed unverified (Read-FinOpsHubData.ps1:72-77,159).
Pulls from .../latest/nuget.exe, executes it, then Add-Type-loads the fetched DLLs with no hash/signature check. Mitigating context: it's an official Microsoft TLS endpoint and the package version is pinned (Parquet.Net -Version 4.24.0), and nuget.exe is Authenticode-signed — but the signature isn't verified here. Pin/verify the nuget.exe hash or validate the Authenticode signature before execution.
9. Bundled tests fail as written.
Test-McpServer.ps1:139 asserts 21 tools (actual: 40); Start-FinOpsMultitool.Tests.ps1:23,28 assert the nonexistent Start-FinOpsMultitool.ps1 / gui/MainWindow.xaml. These need updating once the entry point and tool count are settled. The write modules also have 0 SupportsShouldProcess, which the repo lint may flag.
Minor
- Audit log defaults to
%TEMP%(Confirm-WriteAction.ps1:63) — an "append-only" trail in a user-clearable temp dir weakens the audit story. Consider a more durable default location.
Net: the core surfaces work and the design of the write-safety gate is sound — the problem is that one write tool skips it, the default mode is permissive, and the docs/entry-point don't match reality. Fix #1–#4 and correct the read-only claims and I think this is in good shape. Happy to re-review once those land, and thanks again for the contribution.
|
I did another pass specifically against a large local FinOps hub dataset and want to call out a scaling concern with the current hub-data path. The current "FinOps Hub" fast path appears to be PowerShell over storage exports: it detects a hub storage account, downloads For reference, the local hub dataset we just validated was ~41.9 GB of exported files, 734 Parquet files, ~322M source rows, and 644M rows across raw + transformed Kusto tables. Loading that shape as For larger hubs, the tool should prefer querying the ADX/Fabric/Kusto Hub database directly and push aggregation/filtering into the engine ( This also matters for the new local-hub/on-own-hardware scenario: the local Kusto endpoint can query the full dataset successfully, but this PR currently has no direct Kusto/Hub query path to use it. |
|
One clarification to the recommendation above: if the goal is offline/local analysis and you want to keep the data on your own machine, the better scalable path is the ftklocal approach — load the exports into the local Kusto emulator and query the local Hub database there. So I would frame the options as:
The important point is the same in both deployed and local cases: avoid loading tens of GB / hundreds of millions of rows into PowerShell objects. Push the aggregation into Kusto and bring back summarized results. |
🛠️ Description
Adds the Azure FinOps Multitool to the FinOps toolkit. Discussed with @MSBrett, who suggested contributing the tool into the official toolkit.
The Multitool scans an Azure tenant for cost optimization, governance, and FinOps insights — cost trends, orphaned resources, idle VMs, tag hygiene, reservation/savings-plan utilization, AHB opportunities, budgets, anomaly alerts, and policy compliance. Analysis scans are read-only (Reader / Cost Management Reader) and ground their findings in the customer's live resource state. Four write/remediation tools (delete orphaned resource, deallocate idle VM, enable AHB, set cost allocation rule) are dry-run by default, gated by a configurable write-safety policy, and disabled unless
FINOPS_WRITE_MODEis set — the server defaults toReadOnly.This PR delivers two interfaces over one shared scanner engine, so the same scan logic is reused everywhere:
Invoke-FinOpsMultitoolStart-McpServer.ps1Shared scanner modules (
modules/)30 modular scanners (one per category): orphaned resources, idle VMs, storage tier advice, AHB, tag inventory/recommendations, policy inventory/recommendations, cost data/trend/by-tag, resource costs, reservation advice, commitment utilization, savings realized, budget status, anomaly alerts, Advisor optimization advice, billing structure, contract info, tenant hierarchy, and more.
FinOps Hub data paths
When a FinOps Hub is present, cost scans prefer the hub's Kusto database — an Azure Data Explorer / Fabric cluster (auto-discovered via Resource Graph) or a local ftklocal emulator (
FINOPS_HUB_KUSTO_URI) — and push aggregation into the engine, returning only summarized results. This scales to large hubs (tens of GB / hundreds of millions of rows) without loading raw cost rows into PowerShell. The storage-export reader remains as a small-dataset convenience fallback, used only when no Kusto cluster is reachable.MCP server + agent skills
Start-McpServer.ps1exposes the scanners as 40 MCP tools — 36 read-only (30scan_*, plusrun_full_scan,detect_cost_data_source,get_azure_context, and other helpers) and 4 gated write/remediation tools — over the 2024-11-05 MCP protocol via stdio..vscode/mcp.jsonregisters the server for VS Code, andTest-McpServer.ps1provides protocol-level unit tests.A companion agent-skill ecosystem (
src/templates/agent-skills/) teaches AI agents to use the server proactively and to route findings into the wider FinOps practice. Thefinops-multitoolskill acts as the hub, handing off to 11 FinOps-adjacent skills:power-bi-finops,cost-allocation,azure-policy-governance,unit-economics,finops-reporting,azure-workbooks-finops,forecasting-budgeting,anomaly-investigation,focus-data-quality,sustainability-carbon, andrate-optimization-portfolio.📦 Files added / changed
Public/Start-FinOpsMultitool.ps1Invoke-FinOpsMultitool.ps1+FinOpsMultitool.psm1Start-McpServer.ps1Test-McpServer.ps1modules/helpers/Get-FOHubProvider.ps1+Invoke-FOHubKustoQuery.ps1helpers/Confirm-WriteAction.ps1agent-skills/finops-multitool/agent-skills/cost-data-source/agent-skills/{power-bi-finops, cost-allocation, …}/.vscode/mcp.jsonTests/Unit/Start-FinOpsMultitool.Tests.ps1+FOHubProvider.Tests.ps1docs-mslearn/.../powershell/multitool/+docs/multitool.md📸 Screenshots
Screenshots are in the public repo README.
📋 Checklist
🧪 How did you test this change?
🐳 Deploy to test?
N/A — standalone PowerShell tooling (TUI / MCP server), not a template deployment.
🏷️ Do any of the following that apply?
📄 Did you update
docs/changelog.md?📖 Did you update documentation?
docs-mslearn/.../powershell/multitool/, a Jekyll landing page, overview/TOC/changelog entries, and the module README +finops-multitool/cost-data-sourceskills.