Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3c89c43
feat: add Start-FinOpsMultitool cmdlet for interactive FinOps scanner…
May 19, 2026
1a63159
feat(multitool): cost access warnings, tag query fixes, MG hierarchy …
May 27, 2026
8db8eaa
perf(multitool): optimize tag cost query pacing and filter low-covera…
May 27, 2026
981e4c9
perf: parallel per-sub cost queries in Get-CostByTag
May 27, 2026
d3d7c1f
fix: filter Azure system tags in Get-CostByTag
May 27, 2026
a5fd3e3
fix: dynamic tag cap, remove Contact from system filter
May 27, 2026
54129e2
Add TUI, Hub data routing, and display fixes for FinOps Multitool
May 28, 2026
35790a6
Add TUI README
May 28, 2026
9455685
Add contextual FinOps guidance after each scan result
May 28, 2026
e65b62d
Add tenant picker to TUI for multi-tenant support
May 28, 2026
f09870d
Auto-invoke when script is run directly
May 28, 2026
c735364
Severity-colored guidance with FinOps education context
May 28, 2026
a5e4190
Enrich Hub cost data with live forecast from Cost Management API
May 28, 2026
c976654
Colorize dollar amounts green, budget rows by risk severity
May 28, 2026
1fbc9c0
Fix variable collision: forecast total was overwriting scan count
May 28, 2026
92d17d9
Fix forecast: show full-month projection (actual + remaining forecast)
May 28, 2026
caf4cb2
Fix tag coverage: query ARG for true total/untagged counts when using…
May 28, 2026
62e27b8
Remove Hub coverage caveat from tag guidance, lower resource cost thr…
May 28, 2026
e730754
Fix Cost by Tag guidance: use max untagged cost per tag instead of su…
May 28, 2026
74127a7
Add permission context and diagnostics for missing scan data
May 28, 2026
af92edf
Update TUI README with multi-tenant, guidance, permissions, and Hub e…
May 28, 2026
5215c38
Add styled HTML report export, pre-populated export path, enhanced su…
May 28, 2026
a8654bf
Add MCP server exposing all scan modules as AI-callable tools
May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/powershell/Private/FinOpsMultitool/FinOpsMultitool.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

###########################################################################
# FINOPSMULTITOOL.PSM1
# MODULE LOADER FOR STANDALONE (NON-GUI) USE
###########################################################################
# Purpose: Dot-sources all helpers and analysis modules so they can be
# imported via Import-Module without launching the WPF GUI.
#
# Usage:
# Import-Module .\FinOpsMultitool.psm1
# $results = Get-OrphanedResources -Subscriptions $subs -TenantId $tid
#
# The GUI entry point remains Start-FinOpsMultitool.ps1 (unchanged).
###########################################################################

# -- Ensure required Az modules are loaded ---------------------------------
# Some terminals have incomplete PSModulePath — add standard user module paths
$userModDir = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell\Modules'
if ($userModDir -and (Test-Path $userModDir) -and $env:PSModulePath -notlike "*$userModDir*") {
$env:PSModulePath = "$userModDir;$env:PSModulePath"
}
$userModDir5 = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'WindowsPowerShell\Modules'
if ($userModDir5 -and (Test-Path $userModDir5) -and $env:PSModulePath -notlike "*$userModDir5*") {
$env:PSModulePath = "$userModDir5;$env:PSModulePath"
}

foreach ($azMod in @('Az.Accounts', 'Az.Storage', 'Az.ResourceGraph')) {
if (-not (Get-Module $azMod)) {
Import-Module $azMod -ErrorAction SilentlyContinue
}
}

# -- Helpers (runspace pool, REST retry, ARG wrapper, MG-scope state) ----
$helpersPath = Join-Path $PSScriptRoot 'modules\helpers'
. (Join-Path $helpersPath 'Get-PlainAccessToken.ps1')
. (Join-Path $helpersPath 'Invoke-AzRestMethodWithRetry.ps1')
. (Join-Path $helpersPath 'Search-AzGraphSafe.ps1')
. (Join-Path $helpersPath 'MgCostScope.ps1')
. (Join-Path $helpersPath 'Read-FinOpsHubData.ps1')

# -- Set script-scope root (some modules reference $script:ScriptRootDir) -
$script:ScriptRootDir = $PSScriptRoot

# -- Analysis Modules ----------------------------------------------------
$modulePath = Join-Path $PSScriptRoot 'modules'
. (Join-Path $modulePath 'Initialize-Scanner.ps1')
. (Join-Path $modulePath 'Get-TenantHierarchy.ps1')
. (Join-Path $modulePath 'Get-ContractInfo.ps1')
. (Join-Path $modulePath 'Get-CostData.ps1')
. (Join-Path $modulePath 'Get-ResourceCosts.ps1')
. (Join-Path $modulePath 'Get-TagInventory.ps1')
. (Join-Path $modulePath 'Get-CostByTag.ps1')
. (Join-Path $modulePath 'Get-AHBOpportunities.ps1')
. (Join-Path $modulePath 'Get-ReservationAdvice.ps1')
. (Join-Path $modulePath 'Get-OptimizationAdvice.ps1')
. (Join-Path $modulePath 'Get-TagRecommendations.ps1')
. (Join-Path $modulePath 'Get-CostTrend.ps1')
. (Join-Path $modulePath 'Deploy-ResourceTag.ps1')
. (Join-Path $modulePath 'Get-BillingStructure.ps1')
. (Join-Path $modulePath 'Get-CommitmentUtilization.ps1')
. (Join-Path $modulePath 'Get-OrphanedResources.ps1')
. (Join-Path $modulePath 'Get-BudgetStatus.ps1')
. (Join-Path $modulePath 'Get-AnomalyAlerts.ps1')
. (Join-Path $modulePath 'Get-SavingsRealized.ps1')
. (Join-Path $modulePath 'Get-PolicyInventory.ps1')
. (Join-Path $modulePath 'Get-PolicyRecommendations.ps1')
. (Join-Path $modulePath 'Deploy-PolicyAssignment.ps1')
. (Join-Path $modulePath 'Get-StorageTierAdvice.ps1')
. (Join-Path $modulePath 'Get-IdleVMs.ps1')
2,085 changes: 2,085 additions & 0 deletions src/powershell/Private/FinOpsMultitool/Invoke-FinOpsMultitool.ps1

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions src/powershell/Private/FinOpsMultitool/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Zac Larsen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
279 changes: 279 additions & 0 deletions src/powershell/Private/FinOpsMultitool/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# FinOps Multitool — Terminal UI (TUI)

Interactive terminal interface for running FinOps scans against Azure subscriptions. No GUI dependencies — works in any terminal on Windows, macOS, and Linux.

## Quick Start

```powershell
# From the FinOpsMultitool directory
Import-Module .\FinOpsMultitool.psm1
Invoke-FinOpsMultitool
```

Or target a specific subscription:

```powershell
Invoke-FinOpsMultitool -SubscriptionId '2693c348-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
```

## Requirements

| Requirement | Details |
| --------------------- | --------------------------------------------------------------- |
| PowerShell | 5.1+ (Windows) or 7+ (cross-platform) |
| Az modules | `Az.Accounts`, `Az.Resources`, `Az.ResourceGraph`, `Az.Storage` |
| Azure RBAC | Reader + Cost Management Reader on target scope |
| FinOps Hub (optional) | Storage Blob Data Reader on Hub storage account |

Install Az modules if needed:

```powershell
Install-Module Az.Accounts, Az.Resources, Az.ResourceGraph, Az.Storage -Scope CurrentUser
```

## How It Works

### 1. Authentication

On launch, the TUI checks for an existing `Az.Accounts` session. If you're not logged in, it prompts you to run `Connect-AzAccount`. If your account has access to multiple Azure AD tenants, a tenant picker appears so you can select which tenant to scan. It then discovers all accessible subscriptions and lets you select which ones to scan.

### 2. Data Source Selection

If a FinOps Hub is detected in any of your subscriptions, you'll be asked to choose a data source:

| Source | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **FinOps Hub** | Reads pre-exported FOCUS cost data from Hub storage. Faster, no API throttling. Tag and cost-by-tag scans are instant. |
| **Cost Management API** | Queries the Cost Management REST API in real-time. Slower but always current. Hub tag data is still used when available. |
| **Resource Graph only** | Skips all cost APIs. Only runs scans that use Azure Resource Graph (orphaned resources, idle VMs, etc). |

### 3. Scan Selection

Arrow-key driven menu to toggle individual scans on/off. All scans are selected by default except Billing Structure.

| Key | Action |
| --------- | ------------------ |
| `↑` / `↓` | Navigate scan list |
| `Space` | Toggle scan on/off |
| `A` | Select all |
| `N` | Deselect all |
| `Enter` | Run selected scans |
| `Q` | Quit |

### 4. Scan Execution

Selected scans run sequentially with a progress bar. When a FinOps Hub is available, tag-related scans (Tag Inventory, Cost by Tag) use pre-loaded Hub data instead of API calls — completing in under a second.

### 5. Results

Results display inline with formatted tables, severity-colored guidance, and permission diagnostics.

**Guidance system** — After each scan result, contextual FinOps guidance appears with severity-based coloring:

| Icon | Color | Meaning |
| ----- | ------ | ---------------------------------- |
| `[!]` | Red | Critical finding — action required |
| `[~]` | Yellow | Warning — improvement recommended |
| `[+]` | Green | Healthy — good practices confirmed |

Guidance includes FinOps Foundation best practices, actionable next steps, and links to Microsoft Learn documentation.

**Dollar colorization** — All dollar amounts in results are highlighted in green for quick scanning. Budget rows are colored by risk severity (red for over budget, yellow for at risk, green for on track).

**Permission diagnostics** — When a scan returns no data, the TUI explains why:

- **Access denied** (403/401) — Shows the exact error, required RBAC role, scope, and API
- **No data** — Explains whether the module requires specific resources (e.g., "Returns empty if no budgets are configured")

An optional CSV/JSON export saves to the output path.

## Required Permissions

Each scan module requires specific Azure RBAC roles. The TUI will tell you which role is needed if a scan fails due to missing permissions.

| Category | Scans | Required Role | Scope |
| ------------ | ------------------------------------------------------------ | ------------------------ | ------------------- |
| Optimization | Orphaned Resources, Idle VMs, Storage Tier Advice, AHB | Reader | Subscription |
| Governance | Tag Inventory, Tag Recommendations, Policy Inventory/Recs | Reader | Subscription |
| Cost | Cost Data, Resource Costs, Cost by Tag, Cost Trend | Cost Management Reader | Subscription or MG |
| Commitments | Reservation Advice, Commitment Utilization, Savings Realized | Cost Management Reader | Subscription |
| Monitoring | Budget Status, Anomaly Alerts | Cost Management Reader | Subscription |
| Advisor | Optimization Advice | Reader | Subscription |
| Account | Billing Structure, Contract Info | Billing Reader | Billing Account |
| Hub (opt.) | All scans via Hub data | Storage Blob Data Reader | Hub Storage Account |

## Available Scans

### Optimization (Resource Graph)

| Scan | What it finds |
| ------------------- | ---------------------------------------------- |
| Orphaned Resources | Unattached disks, NICs, public IPs, NSGs |
| Idle VMs | VMs with <5% CPU over 30 days |
| Storage Tier Advice | Blob storage that could move to cooler tiers |
| AHB Opportunities | Windows/SQL VMs not using Azure Hybrid Benefit |

### Governance

| Scan | What it finds |
| ---------------------- | --------------------------------------------------------- |
| Tag Inventory | All tags across resources — names, values, coverage % |
| Tag Recommendations | Inconsistent casing, similar names, missing standard tags |
| Policy Inventory | Azure Policy assignments with scope and compliance |
| Policy Recommendations | Gaps in policy coverage for cost governance |

### Cost Analysis

| Scan | What it finds |
| -------------- | --------------------------------- |
| Cost Data | Monthly spend per subscription |
| Resource Costs | Top resources by cost |
| Cost by Tag | Spend breakdown by tag key/value |
| Cost Trend | Month-over-month spend comparison |

### Commitments

| Scan | What it finds |
| ---------------------- | ---------------------------------------- |
| Reservation Advice | RI purchase recommendations from Advisor |
| Commitment Utilization | RI and Savings Plan usage rates |
| Savings Realized | Actual savings from existing commitments |

### Monitoring

| Scan | What it finds |
| -------------- | --------------------------------- |
| Budget Status | Budget consumption vs. thresholds |
| Anomaly Alerts | Recent cost anomaly detections |

### Advisor & Account

| Scan | What it finds |
| ------------------- | ---------------------------------------- |
| Optimization Advice | Azure Advisor cost recommendations |
| Billing Structure | Account hierarchy and enrollment details |
| Contract Info | Agreement type, offer, support plan |

## FinOps Hub Integration

When a Hub is detected, the TUI automatically loads FOCUS cost data from the Hub's storage account. This enables:

- **Instant tag scans** — Tag Inventory and Cost by Tag read directly from Hub CSV/parquet data instead of querying the Cost Management API
- **No API throttling** — Hub data is read from Azure Storage, avoiding Cost Management API rate limits
- **Richer tag data** — Hub exports contain the full Tags JSON per cost record, enabling accurate per-resource tag parsing
- **Forecast enrichment** — Hub data contains actuals only, so the TUI calls the Cost Management Forecast API to project full-month costs and adds them to Hub actuals
- **Accurate tag coverage** — Hub only sees resources with cost data. The TUI queries Azure Resource Graph for the true total/untagged resource count and overrides the Hub-derived coverage percentage

Hub data is loaded once at startup and reused across all scans that need it.

## Scripting (Non-Interactive)

The scan modules can be called directly without the TUI:

```powershell
Import-Module .\FinOpsMultitool.psm1

# Run a single scan
$tags = Get-TagInventory -Subscriptions $subs -TenantId $tid

# Read Hub data and convert
$hubData = Read-FinOpsHubData -StorageAccountName 'myhub' -ResourceGroupName 'rg-hub' -Months 1
$tagInventory = ConvertTo-TagInventoryFromHub -HubData $hubData
$costByTag = ConvertTo-CostByTagFromHub -HubData $hubData -ExistingTags $tagInventory.TagNames
```

## MCP Server (AI Integration)

The FinOps Multitool includes an MCP (Model Context Protocol) server that exposes all 20 scan modules as AI-callable tools. This lets Copilot, Claude, custom agents, and SRE automation call the same functions used by the TUI and GUI.

### Setup

Add to your VS Code `settings.json` or `.vscode/mcp.json`:

```json
{
"mcp": {
"servers": {
"finops-multitool": {
"command": "pwsh",
"args": ["-NoProfile", "-File", "path/to/Start-McpServer.ps1"]
}
}
}
}
```

### Available Tools

| Tool | Description |
| ---- | ----------- |
| `scan_orphaned_resources` | Find unattached disks, NICs, public IPs, NSGs |
| `scan_idle_vms` | Find VMs with <5% CPU over 14-30 days |
| `scan_storage_tier_advice` | Storage accounts that could use cooler tiers |
| `scan_ahb_opportunities` | VMs/SQL not using Azure Hybrid Benefit |
| `scan_tag_inventory` | Tag coverage %, tag names, resource counts |
| `scan_tag_recommendations` | Inconsistent casing, missing standard tags |
| `scan_policy_inventory` | Policy assignments with compliance status |
| `scan_policy_recommendations` | Policy coverage gaps for cost governance |
| `scan_cost_data` | Actual + forecasted cost per subscription |
| `scan_resource_costs` | Top resources by cost (MTD) |
| `scan_cost_by_tag` | Spend breakdown by tag key/value |
| `scan_cost_trend` | Month-over-month spend comparison |
| `scan_reservation_advice` | RI purchase recommendations |
| `scan_commitment_utilization` | RI and Savings Plan usage rates |
| `scan_savings_realized` | Actual savings from commitments |
| `scan_budget_status` | Budget consumption vs thresholds |
| `scan_anomaly_alerts` | Recent cost anomaly detections |
| `scan_optimization_advice` | Azure Advisor cost recommendations |
| `scan_billing_structure` | Billing account hierarchy |
| `scan_contract_info` | Agreement type, offer, support plan |
| `run_full_scan` | Run all modules — comprehensive assessment |

### Resources

| URI | Description |
| --- | ----------- |
| `finops://permissions` | Required RBAC roles per scan module |
| `finops://modules` | List of all available scan modules |

### Architecture

```
AI Agent (Copilot / Claude / SRE Agent)
│ MCP Protocol (stdio JSON-RPC)
Start-McpServer.ps1
│ Imports FinOpsMultitool.psm1
Get-CostData, Get-TagInventory, etc.
│ Same functions used by TUI and GUI
Azure APIs (Cost Management, Resource Graph, Advisor, etc.)
```

## File Structure

```
FinOpsMultitool/
├── README.md # This file
├── FinOpsMultitool.psm1 # Module loader (dot-sources all scan modules)
├── Invoke-FinOpsMultitool.ps1 # TUI entry point
├── Start-FinOpsMultitool.ps1 # GUI entry point (WPF/XAML, Windows only)
├── Start-McpServer.ps1 # MCP server (AI integration, stdio JSON-RPC)
├── modules/
│ ├── helpers/
│ │ ├── Read-FinOpsHubData.ps1 # Hub storage reader + converters
│ │ ├── Get-PlainAccessToken.ps1 # Token helper
│ │ ├── Invoke-AzRestMethodWithRetry.ps1 # REST retry logic
│ │ ├── Search-AzGraphSafe.ps1 # ARG query wrapper
│ │ └── MgCostScope.ps1 # Management group scope state
│ ├── Initialize-Scanner.ps1
│ ├── Get-CostData.ps1
│ ├── Get-ResourceCosts.ps1
│ ├── Get-TagInventory.ps1
│ ├── Get-CostByTag.ps1
│ ├── Get-OrphanedResources.ps1
│ ├── Get-IdleVMs.ps1
│ └── ... # One file per scan module
└── gui/ # WPF/XAML assets for GUI mode
```
Loading