Skip to content

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

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

Zac Larsen (z-larsen) wants to merge 153 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
Zac Larsen added 11 commits September 16, 2026 00:28
- Document that -SubscriptionId returns an error when the subscription
  cannot be resolved and nothing can answer a prompt, rather than
  scanning every subscription.
- Refresh ms.date on the six pages this pull request changes. The
  update workflow is skipped for fork pull requests.
- Correct full-month forecast windows, column matching, and row totals.
- Flag actual-only forecast fallbacks.
- Keep measured zero utilization without counting absent commitments.
- Restrict export totals to selected subscriptions.
- Reject incomplete pages, required-query failures, and invalid costs.
- Preserve POST bodies and discard failed attempts before retries.
- Calculate hourly vCPU cost from the captured UTC reporting window.
- Separate billed and amortized costs; reject invalid amounts and incomplete coverage.
- Preserve subscription scope, currencies, credits, and observed periods.
- Keep unknown budget forecasts and unsupported history unavailable.
- Surface hub source failures and unverified financial KPIs.
- Respect explicit data-source selection and retain Kusto provenance.
- Validate savings currencies and exclude non-usage charges.
- Separate month-to-date commitment estimates from the AHB estimate.
- Export nested CSV summaries once and use invariant amounts and ISO dates.
- Align public help and documentation with the Microsoft style guide.
- Add source, savings, export, and pagination regression coverage.

Validation: 2812 unit tests passed (7 skipped), 7144 lint checks passed,
and no ScriptAnalyzer findings. Two independent reviews approved the batch.
Successful online Hub validation remains blocked by a 403 response.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [AI][Claude Code] PR Review

Summary: Ambitious, well-organized addition (108 files, 30 read-only scanner modules, terminal UI, 12 new agent skills). Read-only guarantee holds everywhere it was checked — no mutating Azure calls found in any reviewed file. The dominant issue is a systemic pattern: several modules sum Azure Cost Management amounts across subscriptions without checking currency consistency first, unlike the project's own Get-SavingsRealized.ps1/Get-CostTrend.ps1/Get-CostData.ps1, which correctly throw or track per-currency. In an EA/MCA tenant with subscriptions billed in different currencies, this silently produces wrong dollar totals. There's also one execution-verified macOS bug (8 failing Pester tests) and a fail-fast bug that discards an entire scan's results on the first subscription access error.

Pester run on this branch: 526 passed, 8 failed, 7 skipped — all 8 failures trace to two root causes documented below.

Three smaller, non-blocking polish items were split out to #2335 (v16 milestone) rather than posted inline, since they're real but not urgent: a misattributed diagnostic message, a missing static test-guard for the read-only invariant, and missing comment-based help on the primary TUI entry point.

🚫 Blockers (6)

  1. Get-CostByTag.ps1 — a single failed subscription throws and discards the entire scan's results.
  2. Get-CostByTag.ps1 — sums cost across subscriptions/currencies, labels the total with whichever currency was seen last.
  3. Get-AIWorkloadMetrics.ps1 — same cross-currency summing bug in AI cost-per-token/request metrics.
  4. Get-SharedCostAllocation.ps1 — the FinOps Hub data path has no currency-mismatch check (the Live API path in the same function does).
  5. Get-UnitEconomics.ps1 — currency is correctly flagged "Mixed", but the cost/unit-economics numbers next to it aren't gated on that flag.
  6. Read-FinOpsHubData.ps1 — macOS stat -f '%Lp' drops the sticky bit, breaking private-directory validation for any path under a sticky world-writable ancestor (e.g. /tmp). Execution-verified via the failing Pester run above.

⚠️ Should fix (15)

7-8. Same cross-currency summing pattern in Get-OptimizationAdvice.ps1 and Get-ReservationAdvice.ps1 (Advisor savings estimates).
9-12. Missing nextLink pagination: Get-BillingStructure.ps1 (5 calls), Get-MaccCommitment.ps1 (2 calls), Get-BillingAccount.ps1, MgCostScope.ps1.
13-15. Weak/silent failure logging: Get-TagInventory.ps1, Get-PolicyInventory.ps1, Get-ContractInfo.ps1.
16-17. Invoke-FinOpsMultitool.ps1 — silent Hub-detection failure swallowing, and a shared catch that mislabels an untried scan as failed.
18. MultitoolSafety.Tests.ps1 — the "no undefined commands" self-test false-fails on non-Windows (one of the 8 real Pester failures above).
19. Test coverage: 16 of 30 scanner modules have no test exercising their own domain logic, despite the PR checklist marking unit tests as done.
20. finops-reporting/SKILL.md references a content-humanizer skill that doesn't exist anywhere in the repo.
21. finops-multitool-commands.md's scan-coverage table doesn't disclose that 4 of the 30 modules aren't reachable through the documented menu or -Scans parameter.

💡 Suggestions (8)

Minor: stale .gitignore entry, a malformed survey URL, changelog tense inconsistency, a duplicated bullet, an unjustified tolower(), a USD-only savings figure with no currency label, dead code, and the same hub-path currency pattern as the blockers (lower risk here, but worth the same fix while touching this code).

Comment thread src/powershell/Private/FinOpsMultitool/modules/Get-CostByTag.ps1 Outdated
Comment thread src/powershell/Private/FinOpsMultitool/modules/Get-CostByTag.ps1
Comment thread src/powershell/Private/FinOpsMultitool/modules/Get-UnitEconomics.ps1 Outdated
Comment on lines +961 to +972
elseif ($DataSource.Source -eq 'Hub' -and $DataSource.HubStorage) {
# Storage reader: small-dataset convenience path (rows loaded into
# PowerShell). For large hubs, the Kusto path above is preferred.
$hub = $DataSource.HubStorage
if ($DataSource.Source -eq 'Hub') {
Write-FinOpsConsole ""
Write-FinOpsConsole " Loading cost data from FinOps Hub storage (small-dataset reader)..." -ForegroundColor Green
Write-FinOpsConsole " For large hubs, query the Kusto database instead (ADX/Fabric, or set FINOPS_HUB_KUSTO_URI for ftklocal)." -ForegroundColor DarkGray
}
else {
Write-FinOpsConsole " Loading Hub tag data for fast tag scans..." -ForegroundColor DarkGray
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [AI][Claude Code] 💡 Suggestion

Inside elseif ($DataSource.Source -eq 'Hub' -and $DataSource.HubStorage), the nested if ($DataSource.Source -eq 'Hub') {...} else {...} (966-972) has an unreachable else branch, since the outer condition already guarantees Source -eq 'Hub'. No functional impact, but it reads like a "fast tag scan only" code path that no longer exists — leftover from an earlier refactor.

Comment thread .gitignore Outdated
Comment thread docs-mslearn/toolkit/powershell/multitool/finops-multitool-commands.md Outdated
Comment thread docs-mslearn/toolkit/changelog.md Outdated
Comment thread src/templates/agent-skills/cost-data-source/SKILL.md Outdated
- Preserve partial cost coverage and currency-safe monetary estimates.
- Retain billing pagination and membership lookup failures.
- Keep missing inventory, utilization, and carbon evidence unavailable.
- Reconcile small allocations and preserve macOS permission bits.
- Fix Parquet row mapping and case-sensitive module packaging.
- Add real reader and packaged-launch tests with platform CI evidence.
- Clarify documentation and financial units in agent skills.
@z-larsen

Zac Larsen (z-larsen) commented Sep 24, 2026 •

Copy link
Copy Markdown
Author

Published ae36741e with the September 23-24 review fixes and regression coverage.

Follow-up f11f8611 moves the runner-local temporary directory setting to the test steps. GitHub rejected the original job-level runner.temp expression before starting any tests. The corrected workflow passed actionlint 1.7.12; hosted test results remain pending.

Review follow-up

Concern Change and regression evidence
Incomplete tag maps and failed subscription cost queries Complete tag-map reads are required; successful subscription costs remain explicitly partial when another subscription fails. Failed continuation pages contribute no partial amounts. Covered in CostQueryPagination.Tests.ps1.
AI and unit-economics monetary rates Missing or mixed currencies suppress money, not measured usage or capacity. AI rates use matching account costs and usage. Covered in CostQueryPagination.Tests.ps1 and MultitoolSafety.Tests.ps1.
Advisor and reservation savings Recommendations retain their currencies; incompatible totals remain unavailable. Covered in CurrencyLabel.Tests.ps1 and the real consumer/report checks in MultitoolSafety.Tests.ps1.
Billing pagination and membership failures Account, hierarchy, rule, and MACC reads follow validated pages. Failed or empty membership responses remain visible through the resolver and consumers. Covered in CostQueryPagination.Tests.ps1.
Incomplete inventory and measurement evidence Tags, policy, alerts, idle-VM metrics, and carbon measurements no longer turn failed reads into healthy or zero results. Covered in MultitoolSafety.Tests.ps1 and PolicyEffect.Tests.ps1.
Hub discovery and converter isolation Failed discovery requires an explicit alternative; a failed converter doesn't suppress unrelated scans. Covered through the public launcher in Start-FinOpsMultitool.Tests.ps1.
Allocation integrity Case-insensitive spoke deduplication and cent reconciliation prevent duplicate or negative allocations. The existing Hub currency guard is retained and tested rather than duplicated. Covered in MultitoolSafety.Tests.ps1.
Native filesystem behavior BSD permission checks retain the sticky bit; Windows-only command checks are scoped appropriately. Covered in ParquetPackageClient.Tests.ps1 and MultitoolSafety.Tests.ps1.
Real Parquet loading Integration testing exposed and fixed null row values and nested-schema column misalignment. MultitoolParquet.Tests.ps1 uses real pinned packages, signatures, payload checks, assembly loading, and separate cold/cached processes with Snappy and multiple row groups.
Distributable packaging Fixed directory casing and added an isolated build, manifest import, public launch, and CSV/HTML/text checks in MultitoolPackage.Tests.ps1. Azure calls use synthetic responses.

Local validation

Windows, PowerShell 7.6.6, Pester 6.0.0:

  • Full unit suite: 3,049 passed, 0 failed, 8 skipped.
  • Full lint suite: 7,144 passed, 0 failed.
  • Package and real Parquet integration: 5 passed, 0 failed, 0 skipped.
  • The final README-only cleanup passed all 1,568 documentation-link tests. These are part of the unit suite, not additional coverage counts.
  • Changed-script parsing, analyzer checks, and staged whitespace checks passed.

CI and remaining limits

The workflow includes Windows, Ubuntu, and macOS multitool jobs, with NUnit artifacts and summaries identifying the tested commit and host. No Azure sign-in or deployment credentials are used. Hosted Linux/macOS results are not yet available; local Windows results don't establish native compatibility.

macOS runs the native unit and packaged-launch checks. Microsoft documents NuGet signed-package verification as unsupported on macOS, so real signed Parquet integration runs on Windows and Ubuntu. Production signature verification remains enforced; no bypass was added. Kusto or available CSV exports are the alternatives on macOS.

The integration tests don't prove live Azure coverage. A new VM test package is ready; live validation of the current build remains pending.

The three follow-ups in #2335 remain deferred to v16: storage error classification, a static read-only scanner guard, and private launcher help. This update doesn't claim to close them.

The README retains usage, limitations, and reusable test instructions. PR-specific tracking is kept in this comment instead.

Zac Larsen added 2 commits September 24, 2026 11:31
Add a searchable KPI reference with formulas, inputs, interpretation and limitations. Clarify cost-share denominators, measurement periods and forecast availability. Preserve measured zero unit rates and report export when the KPI catalog is unavailable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 [AI][Claude Code] PR Review — verification of the 2026-09-24 fix commits

Follow-up to the 2026-09-23 review. The fix commits (ae36741 et al.) addressed nearly everything from that review — verified via 4 independent passes, each re-checking a slice of the diff against the original findings, plus a fresh look at files outside the original scope. Full current test suite: 2940 passed, 0 failed.

What's confirmed fixed and correct: all 4 currency-summing bugs (Get-CostByTag.ps1, Get-AIWorkloadMetrics.ps1, Get-UnitEconomics.ps1, plus 2 more found and fixed proactively in Get-OptimizationAdvice.ps1/Get-ReservationAdvice.ps1), the fail-fast bug, the macOS sticky-bit bug (verified empirically on this machine), the mislabeled-scan-failure bug, missing pagination in 5 of 5 flagged files, the stale content-humanizer skill reference, the duplicated SKILL.md bullet, and the non-Windows test false-fail. Scanner test coverage went from 14/30 to 29/30. Great pass overall.

One blocker found in the fix itself: fixing the silent Hub-detection-failure issue introduced two unhandled throws that now crash the whole tool on a transient probe error — including in -NonInteractive/CI mode, where the old code silently (if unhelpfully) fell back. Detail below.

Three should-fix items and three suggestions — all non-blocking gaps or minor issues in the fix commits — were added to the existing tracking issue rather than posted here: #2335

🚫 Blockers (1)

  1. Select-DataSource's new error handling for Hub auto-detection throws unhandled in two spots, hard-crashing -NonInteractive/CI runs on a transient probe failure instead of falling back.

Comment thread src/powershell/Private/FinOpsMultitool/Invoke-FinOpsMultitool.ps1 Outdated
- Preserve selected tenant and subscription scope through Hub discovery failures.
- Reconcile AI token totals and account identities with matching UTC cost windows.
- Correct commitment metadata, pagination, and unavailable utilization.
- Add scoped cost trends, resource periods, policy names, and readable local grids.
- Clarify budget sampling and CPU units; close deferred diagnostic/help/test gaps.
- Harden credit sorting, CSV formula protection, and the scanner read-only guard.
- Validate 3170 unit tests, 7144 lint checks, runtime parity, and independent review.
@z-larsen

Copy link
Copy Markdown
Author

Review follow-up: fe9ddf1

Copilot-assisted implementation update. Published to this PR's branch and mirrored to the standalone TUI at 7be56ca (15.0-dev-wip.20261001.1).

Fixes and why the reported behavior should no longer recur

Area Change and regression protection
Automatic Hub discovery failures The two latest suggestions (discovery failure, provider exception) are addressed. Automatic discovery can still offer API/GraphOnly after failed probes; a selected storage fallback is retained without a second failing provider lookup. Explicit Hub/Kusto failures remain errors. Tests verify these paths without widening the selected tenant/subscriptions.
Tenant and subscription boundaries Explicit IDs must resolve in the current tenant. Discovery is pinned to the verified context and selected subscriptions; mismatches stop rather than searching other tenants. Management-group discovery is bounded and its cache is tenant-specific.
AI usage and effective rates Account, deployment, and overall token totals now use the same measured basis. Same-named deployments in different accounts remain distinct. Missing samples are unavailable, not zero, and incomplete measurements suppress rates. Monitor usage and amortized cost share a captured UTC window; Hub model rows retain account identity. Currency incompatibility remains a hard limit on combined money values.
Commitment identity and utilization Removed provisioning-state/billing-scope substitutions for SKU/kind. Targeted reservation metadata must match the requested resource ID. All relevant pages are read, duplicate references are removed, and the newest returned period is selected explicitly. Unknown percentages and absent commitment types no longer become 0%; incomplete/unscoped coverage suppresses overall averages.
Cost trends, periods, and names Added selected-scope and per-subscription trend views, captured UTC windows, partial-month labels, and separate empty/unverified coverage. Grouped rows are filtered to the selected IDs. Resource and policy identities retain original IDs alongside verified readable labels. Query windows are not claimed as billing-completeness timestamps.
Existing HTML readability Enhanced the existing tables, rather than adding a redundant viewer: sticky headers, row numbers, local filtering, stable sorting, paging, resizable columns, and expanded native dialogs. All returned rows remain available and CSVs are preserved. Fixed a dialog restoration bug found during browser testing and credit-sort variants found by independent review.
Diagnostic accuracy Budget sampling is labeled as unqueried coverage, not an access denial. CPU values have explicit 14-day/% labels. Storage lookup/network failures are distinguished from authorization failures. Missing definition reads remain visible independently of policy compliance.
Read-only and export safeguards Added an AST read-verb allowlist with explicit read-query/client-context exceptions and mutation self-tests. Hardened CSV formula prefixes after whitespace/invisible characters and reject unsafe generic headers; numeric credits remain numeric. HTML values continue to be escaped.
Deferred tracking items Addressed the outstanding diagnostic/help/test/cleanup items in #2335, including direct AHB meter/cache/fallback tests, private entry-point help, billing response handling, and the inferred-contract note.

Validation performed

  • Full PowerShell unit suite: 3,170 passed, 0 failed, 8 skipped; no not-run cases or failed discovery blocks/containers.
  • Full PowerShell lint suite: 7,144 passed, 0 failed. Changed-file PowerShell parsing and analyzer checks passed.
  • Tests/imports ran in fresh processes with a bound, non-listening loopback proxy installed before imports. No live tenant scans or Azure context changes were used for this validation.
  • Browser checks used fictional data: 85 subscriptions, 9 grids, and 1,168 retained rows. Sorting, filtering (including collapsed details), paging, keyboard resizing, dialog close/cancel restoration, and 20 tab/viewport checks at 1440/1024/390/320 px passed. Desktop and explicitly emulated 390 px screenshots were inspected. Controls require no external assets or uploads.
  • The real embedded JavaScript is executed by the credit-sort regression, including parenthesized/signed credits and unavailable values.
  • Standalone parity passed for 47 shared runtime files, the identical public launcher, and exactly four version overlays.
  • A separate Sonnet 5 security/code review found no blocking findings. Its two medium findings were fixed and reviewed again; its CSV edge-case follow-up was also fixed and re-reviewed. The independent pass was code-only; the executed test results above are from the primary validation process.
  • Audited the complete changed Toolkit contents and standalone publication delta, including its earlier unpublished parity commit. No new candidate secrets, known scan identifiers, local user paths, or report artifacts were found. Two credential-looking URLs are intentional example.test rejection fixtures. No dedicated secret scanner was installed, so this is not a formal scanner certification or a claim that historical fork-network data was purged.

Remaining limits

Live Azure collection and native macOS/Linux execution were not tested in this round. The AST check is a test-time defense, not a proof about every dynamic command or POST endpoint. Billing-scope commitment results can exceed the selected subscription scope and remain explicitly labeled. Maintainer review and approval are still required; this update does not claim merge approval.

Zac Larsen added 4 commits October 2, 2026 07:37
…review findings

Adds an ordinary Cost Management CSV export data source so a scan can run
without deploying a FinOps hub, plus the corrections raised in two
independent reviews of that change.

Added
- New `Export` value on `-DataSource` and a data source menu entry. Discovery
  reads export definitions for the selected subscriptions, their
  management-group ancestors, and linked billing accounts, then always scans
  storage accounts in those subscriptions and merges exports Cost Management
  cannot see, deduped against the definitions.
- Four export-backed cost views: cost totals, resource costs, cost by tag, and
  cost trend. Every other financial scan is rejected with an explicit error and
  never falls back to live Cost Management costs.
- Progress reporting per scope, per subscription, and per storage account, with
  discovery warnings summarized and detail available under -Verbose.

Review findings corrected
- Storage-first discovery was gated behind an empty definition result. That
  reintroduced a previously fixed defect: a cross-tenant scan can return
  definitions for some subscriptions while a central management-group export
  stays invisible, so gating hid it. The gate is removed and a regression test
  now asserts the storage pass runs even when definitions exist.
- Partitioned runs were totalled without consulting manifest.json. A run whose
  parts are partly missing or still being written reported a confident total.
  The declared partitions are now verified and a short read is reported as
  incomplete.
- A blank FOCUS SubAccountId failed the whole read. FOCUS permits null on
  tenant-level charges such as MCA purchases and refunds, which the newly
  discoverable billing-account exports contain. Those rows are now excluded
  from a scoped read and counted; a malformed non-empty ID still fails.
- Resource costs were keyed on resource identifier alone, so a classic export
  carrying a bare instance name merged same-named resources across
  subscriptions. The key now includes the subscription.
- The unattended single-candidate rule counted candidates this path cannot
  read, so one readable CSV beside a Parquet folder failed as ambiguous. It now
  counts readable candidates, and the picker labels the others.
- Trend months were bucketed with the host calendar, so a non-Gregorian culture
  split one month across two rows. The key is now invariant.
- The discovery dedupe key trimmed the root folder on the storage side only, so
  a definition path with a leading slash produced a duplicate candidate.

Documentation
- Corrected overstated claims: reads are limited to the chosen export folder
  rather than generally bounded, only the newest run is read, and the container
  name filter and wider-scope storage destinations are now documented.
- Documented the `Export` value in the public command reference.

Validation
- 3,202 unit tests passed, 0 failed, 8 skipped, offline behind a closed
  loopback proxy.
- 7,144 lint tests passed.
- PSScriptAnalyzer reports 0 errors and warnings on the changed files.
- Runtime parity with the standalone edition verified across 47 files.
- No live Azure calls; all fixtures synthetic.
@z-larsen

Copy link
Copy Markdown
Author

Current-head review update: fba4d01

Copilot-assisted follow-up. The latest changes are in fba4d01 and mirrored to the standalone TUI at 7c39489.

Changes since the last summary

  • Ordinary Cost Management CSV exports now work without a hub. Discovery combines export definitions with storage discovery in the selected subscriptions and discovers container names automatically.
  • The export reader rejects unsafe blob paths before downloading manifests or parts, validates fallback container names, and removes control characters from discovery diagnostics.
  • The policy empty-list fix and the five follow-up fixes are included: retained commitment scope gaps, malformed hub-tag handling, explicit empty-scan rejection, malformed-budget coverage, and budget-history dependency failures.
  • Corrected the shared-engine wording, hub casing, and macOS Parquet guidance. The earlier CRLF report-output fix is included too.

Existing paging wrappers and upstream currency checks remain in place. Storage discovery still runs when export definitions are found, so mixed discovery paths aren't dropped. Failed selected-source reads don't become live API scans, and sampled budget coverage stays unverified.

Verification

  • Windows/Pester 6: 3,252 unit tests passed, 0 failed, 8 skipped; no failed discovery containers or blocks.
  • 7,144 lint tests passed. Changed PowerShell files passed parsing and Error/Warning analyzer checks. The final documentation-link check passed all 1,568 cases.
  • Publication parity passed for 47 runtime files, the public launcher, and four standalone version overlays. The unrelated local standalone renderer edit wasn't published.
  • Opus and Astra independently inspected the review findings. Opus also reviewed the export hardening and its follow-up. Those were source reviews; the executed test results above came from the local validation process.
  • Fresh test processes used the closed-loopback proxy before imports. No live Azure collection or native macOS/Linux execution was performed in this round. Added-content inspection found no candidate secrets or private identifiers; this wasn't a full-history secret scan.

Review status

I checked all 154 threads, replied with current-commit evidence, and resolved 81 verified findings. Seven threads remain open:

The current-head PowerShell Tests workflow is waiting for maintainer approval and has zero jobs, not failing tests. The ms.date and PR Deploy workflows are also awaiting action. No workflows or deployments were approved or dispatched in this round.

Michael Flanakin (@flanakin) Brett Wilson (@MSBrett), could you inspect this head, approve the appropriate validation workflows, and re-review the remaining requests? Thread cleanup doesn't clear the existing changes-requested review or replace maintainer approval.

This branch has not been deployed

No deployments
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.

6 participants