Skip to content

Add Start-FinOpsMultitool — cross-platform terminal UI for FinOps scanning - #2155

Open
Zac larsen (z-larsen) wants to merge 110 commits into
microsoft:devfrom
z-larsen:feature/finops-multitool
Open

Add Start-FinOpsMultitool — cross-platform terminal UI for FinOps scanning#2155
Zac larsen (z-larsen) wants to merge 110 commits into
microsoft:devfrom
z-larsen:feature/finops-multitool

Conversation

@z-larsen

@z-larsen Zac larsen (z-larsen) commented May 19, 2026

Copy link
Copy Markdown

🛠️ Description

Adds the FinOps multitool to the FinOps toolkit. Discussed with Brett Wilson (@MSBrett), who suggested contributing the tool into the official toolkit.

The multitool scans an Azure environment for cost optimization, governance, and FinOps insights — cost trends, orphaned resources, idle VMs, tag hygiene, reservation and savings plan utilization, Azure Hybrid Benefit opportunities, budgets, anomaly alerts, and policy compliance — and grounds its findings in live resource state.

Everything in this PR is read-only. It needs Reader or Cost Management Reader on the target scope and never creates, changes, or deletes a resource. Remediation and the MCP server were split out to a separate branch and will follow as their own PR.

One scanner engine, two consumers:

Interface Entry point Best for
Terminal UI Start-FinOpsMultitool A person who wants a full assessment
Agent skills src/templates/agent-skills/ AI agents answering a single question through az or an Azure MCP server

A separate WPF GUI is maintained outside this repo. This PR contributes the terminal UI and the agent skills.

Running it

The terminal UI uses arrow-key menus when the console supports them. Consoles that can't render those menus — PowerShell remoting sessions, some editor terminals — fall back to numbered prompts, which is also what a screen reader can follow. Both paths run the same scans and produce the same results.

For automation, -NonInteractive with -Scans, -DataSource, -SubscriptionId, and -OutputPath supplies every choice, so the tool runs from a pipeline or a scheduled job:

Start-FinOpsMultitool -NonInteractive `
    -SubscriptionId '00000000-0000-0000-0000-000000000000' `
    -Scans Get-OrphanedResources, Get-IdleVMs `
    -DataSource API `
    -OutputPath './results'

FinOps hub data paths

This addresses Brett Wilson (@MSBrett)'s scaling review. When a FinOps hub is present, cost scans prefer the hub's Kusto database — an Azure Data Explorer or Fabric cluster, auto-discovered through Resource Graph, or a local ftklocal emulator via FINOPS_HUB_KUSTO_URI — and push aggregation into the engine, returning only summarized result sets. Raw cost rows are never materialized in PowerShell on that path.

The storage-export reader remains as a small-dataset fallback rather than the scalable path, and the terminal UI warns before using it on a hub with no reachable cluster, offering the live Cost Management API instead.

Scans

30 scan modules across optimization, governance, cost analysis, commitments, monitoring, Advisor, account, AI and ML, and sustainability. The terminal UI surfaces 26 of them. Results render in the terminal and export to one CSV per scan, a FinOpsReport.html summary, and a ScanSummary.txt file.

📦 Files added / changed

Path Purpose
Public/Start-FinOpsMultitool.ps1 Public cmdlet — launches the cross-platform terminal UI
Invoke-FinOpsMultitool.ps1 + FinOpsMultitool.psm1 Terminal UI + module loader
modules/ 30 read-only scanner modules
helpers/Get-FOHubProvider.ps1 + Invoke-FOHubKustoQuery.ps1 Scalable FinOps hub Kusto data path (ADX / Fabric / ftklocal)
agent-skills/finops-multitool/ + references/ Routing hub skill and its investigation references
agent-skills/cost-data-source/ Cost data-source routing skill (Kusto vs storage vs API)
agent-skills/{power-bi-finops, cost-allocation, …}/ 11 FinOps-adjacent skills
Tests/Unit/Start-FinOpsMultitool.Tests.ps1 + FOHubProvider.Tests.ps1 Pester unit tests
docs-mslearn/.../powershell/multitool/ + docs/multitool.md Documentation (command reference, landing page, TOC, changelog)

📸 Screenshots

Screenshots are in the public repo README.

📋 Checklist

🧪 How did you test this change?

  • 🧹 Lint tests
  • 👍 PS -WhatIf / az validate
  • 🔌 Manually deployed + verified
  • 🧪 Unit tests
  • 👀 Integration tests

🐳 Deploy to test?

N/A — standalone PowerShell tooling, not a template deployment.

🏷️ Do any of the following that apply?

  • 🚨 This is a breaking change.
  • 🐣 The change is less than 20 lines of code.

📄 Did you update docs/changelog.md?

  • ✅ Updated changelog
  • ❌ Log not needed (small/internal change)

📖 Did you update documentation?

  • ✅ Documentation updated — FinOps multitool reference under docs-mslearn/.../powershell/multitool/, a Jekyll landing page, overview/TOC/changelog entries, and the module README plus the finops-multitool and cost-data-source skills.
  • ❌ Docs not needed (small/internal change)

… 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).
@z-larsen

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Microsoft"

@flanakin

Copy link
Copy Markdown
Collaborator

Zac larsen (@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?

https://aka.ms/ftk/contrib-sync

@z-larsen

Copy link
Copy Markdown
Author

Thanks, Michael! Would love to join.

Zac Larsen added 15 commits May 26, 2026 22:11
…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
@microsoft-github-policy-service microsoft-github-policy-service Bot removed the Needs: Review 👀 PR that is ready to be reviewed label Aug 23, 2026
1. TUI data-source selection failed before any scan ran. The local $dataSource
   reused the validated -DataSource parameter, since PowerShell variable names
   are case-insensitive, so assigning the picker hashtable threw
   ValidationMetadataException on every path. Renamed the local to
   $sourceChoice and added an AST regression test asserting no validated
   parameter is ever an assignment target. The test fails on 9a8fcf4.

2. MACC was not bounded by the selected scope. Get-MaccCommitment took
   $Subscriptions but never used it, so it returned commitments from every
   reachable billing account.

3. Billing Structure dropped readable EA enrollments. It relied on
   billingInfo/default, which is not a valid resource type and 404s on all
   api-versions, leaving the linked-account set empty and skipping every
   account.

2 and 3 share a root cause, so both now use a new Get-FinOpsBillingScope
helper: billingProperty/default per subscription first, then the
billingSubscriptions walk Get-ContractInfo already uses. It separates "read it,
no match" from "could not read it" so a permissions gap reports as one.

Get-CommitmentUtilization had the same invalid endpoint and was not flagged.
There the empty set skipped the filter entirely, so it used every reachable MCA
account. Same helper applied.

Still outstanding: rerunning the TUI against the online Hub, local Hub, and API
paths, which needs a live session.
@microsoft-github-policy-service microsoft-github-policy-service Bot added Needs: Review 👀 PR that is ready to be reviewed and removed Needs: Attention 👋 Issue or PR needs to be reviewed by the author or it will be closed due to no activity labels Aug 24, 2026
Zac Larsen added 4 commits August 23, 2026 19:08
# Conflicts:
#	docs-mslearn/toolkit/changelog.md
…resource

Get-CostByTag emitted "(untagged resources)" on the live API path while every
consumer compared against "(untagged)". Nothing matched, so untagged spend
summed to zero. That produced three wrong outputs at once: 0% untagged, 100%
tagging-policy compliant, and guidance claiming no CAF allocation tag was in
use, all while the table above showed real untagged cost under CostCenter,
Customer, Project, and Environment. The hub paths already emitted "(untagged)",
which is why local hub testing never caught it. Normalized to one value.

The KPI also selected the allocation tag with the LOWEST untagged percentage,
so a single well-covered dimension reported the whole estate as compliant. The
guidance selected the highest untagged cost, so the two contradicted each other
despite a comment saying they agreed.

Allocation coverage is now counted once per resource on the live path, where
the loop already has both the cost and the resolved tags: a resource is
allocated if it carries any CAF allocation tag. Per-tag totals cannot answer
this, since a resource appears as untagged under every tag it lacks and summing
double-counts the same spend. Exposed as AllocatedCost, UnallocatedCost, and
ResourceCostSeen, null when a run did not walk resources so consumers can tell
"none allocated" from "not measured".

Sources that aggregate server-side keep a per-tag fallback, now reporting the
worst-covered tag rather than the best. Guidance reads the same aggregate.

Verified against the reported shape: 93 of 571 resource spend unallocated
reports 16.3%, and the old sentinel still reproduces 0%/100%.
The TUI writes exported results to FinOpsResults, so ignore it to keep a local run out of the repository.
@z-larsen

Zac larsen (z-larsen) commented Aug 26, 2026

Copy link
Copy Markdown
Author

Thanks Brett - all three are fixed in b336ab0c.

1. TUI data-source selection fails before scans start
Select-DataSource's return value now lands in $sourceChoice, so it no longer collides with the [ValidateSet] $DataSource parameter. Added an AST regression test in Start-FinOpsMultitool.Tests.ps1 that fails if any ValidateSet parameter is assigned to anywhere in Invoke-FinOpsMultitool - it guards the class of bug rather than this one instance.

2. MACC results are not bounded by the selected scope
Get-MaccCommitment now correlates billing accounts to the selected subscriptions through a shared Get-FinOpsBillingScope helper instead of ignoring $Subscriptions.

3. Billing Structure drops a readable EA enrollment
Dropped the billingInfo/default dependency. It is not a valid resource type and 404s on every api-version, which is what left the account set empty. Get-BillingStructure now lists billing accounts and correlates them through the same Get-FinOpsBillingScope helper Get-ContractInfo uses, and warns when correlation fails.

Unit suite: 2433 passed / 0 failed / 7 skipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed Tool: PowerShell PowerShell scripts and automation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants