@pagopa/dx-savemoney
v0.6.1
Published
Azure resource analyzer for finding unused or cost-inefficient resources.
Readme
DX Save Money
A TypeScript library for analyzing CSP (Cloud Service Provider) resources to identify potential cost inefficiencies and underutilized resources. It operates in read-only mode and does not modify, tag, or delete any resources; instead, it generates detailed reports to support FinOps decisions.
Supported Cloud Providers
✅ Azure
Full support for Azure resource analysis with intelligent detection and flexible reporting.
🚧 AWS (Coming Soon)
AWS support is planned for future releases. The architecture is designed to support multiple CSPs with provider-specific analyzers.
Architecture
The SaveMoney tool follows a modular architecture designed for multi-CSP support:
flowchart LR
subgraph cli["CLI Layer"]
CMDs[other cmds]
CLI[savemoney]
end
subgraph pkg["Package Layer - @pagopa/dx-savemoney"]
Config[Config Loader]
Azure[Azure Analyzer]
AWS[AWS Analyzer<br/>Coming Soon...]
end
subgraph svc["Azure Services"]
ARM[Azure Resource Manager]
Monitor[Azure Monitor Metrics]
Identity[Azure Identity]
end
subgraph rsc["Azure Resources"]
direction LR
VM[Virtual Machines]
Disk[Managed Disks]
NIC[Network Interfaces]
IP[Public IPs]
ASP[App Service Plans]
PE[Private Endpoints]
SA[Storage Accounts]
CA[Container Apps]
SS[Static Web Apps]
end
CLI --> Config
Config --> Azure
Config -.-> AWS
Azure --> ARM
Azure --> Monitor
Azure --> Identity
ARM --> rsc
Monitor --> rsc
style CLI fill:#0078d4,color:#fff
style Azure fill:#0078d4,color:#fff
style AWS fill:#ccc,color:#666
style Config fill:#107c10,color:#fffInstallation
npm install @pagopa/dx-savemoney
# or
pnpm add @pagopa/dx-savemoney
# or
yarn add @pagopa/dx-savemoneyAzure
Main Features
- Multi-Subscription Analysis: Scans multiple Azure subscriptions in a single command.
- Intelligent Detection: Uses Azure Monitor metrics (e.g. CPU, network traffic, transactions) to scientifically identify inactive resources.
- Azure Advisor Integration: Fetches Cost recommendations directly from Azure Advisor, including Reserved Instance and Savings Plan opportunities with estimated monthly savings.
- Orphaned Resource Identification: Detects commonly "forgotten" resources like unattached disks, unassociated public IPs, and unused network interfaces.
- Flexible Reporting: Offers multiple output formats:
table: A human-readable summary for the terminal.json: Standard format for integration with other tools.detailed-json: A comprehensive output with all resource metadata, ideal for in-depth analysis via AI or custom scripts.lint: A linter-style output grouped by resource, with risk icons and a summary — ideal for CI pipelines and quick triage.
- Simplified Configuration: Supports configuration via files, command-line options, environment variables, or an interactive prompt.
- Configurable Thresholds: All analysis thresholds (CPU%, memory, transaction counts, etc.) can be overridden via the
thresholdssection of the YAML config file. - Tag Filtering: Restrict the analysis to resources that match specific tag key-value pairs.
Analyzed Resources
The tool analyzes the following Azure resource types with specific detection methods and risk levels:
| Resource Type | Detection Method | Cost Risk | What's Checked |
| :---------------------- | :---------------------- | :-------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Virtual Machines | Instance View + Metrics | 🔴 High | Deallocated/stopped state, Low CPU usage (<1%), Low network traffic (<3MB per days) |
| App Service Plans | API Details + Metrics | 🔴 High | No apps deployed, Very low CPU (<5%), Very low memory (<10%), Oversized Premium tier |
| Container Apps | API Details + Metrics | 🟡 Medium | Not running state, Zero replicas configured, Low CPU (<0.001 cores), Low memory (<10MB), Low network traffic (<1MB) |
| Managed Disks | API Details | 🟡 Medium | Unattached state, No managedBy property |
| Public IP Addresses | API Details + Metrics | 🟡 Medium | Not associated with any resource, Static IP not in use, Very low network traffic (<~340KB per day) |
| Network Interfaces | API Details | 🟡 Medium | Not attached to VM or Private Endpoint, No public IP assigned |
| Private Endpoints | API Details | 🟡 Medium | No private link connections, Rejected/disconnected connections, No network interfaces |
| Storage Accounts | Metrics | 🟡 Medium | Very low transaction count (<10 per days in timespan) |
| Static Web Apps | Metrics | 🟢 Low | No traffic data available, Very low site hits (<100 requests in 30 days), Very low data transfer (<1MB in 30 days) |
| Azure Advisor | Advisor API | ⚪ Varies | Reserved Instance and Savings Plan opportunities, right-sizing suggestions, and other Cost recommendations — with estimated monthly savings where available |
| AZQR report | --azqr-report JSON | ⚪ Varies | Optional. Billable orphans (cost) and free orphaned resources such as empty subnets or unattached NSGs (cleanup candidates) parsed from an AZQR scan |
Pricing Coverage
SaveMoney keeps two monetary signals separate:
- Retail Prices API estimates the monthly list-price cost at risk for custom findings on resources with predictable pricing.
- Azure Advisor reports Microsoft-provided savings for Cost recommendations when available.
| Resources | Pricing source | Notes |
| :---------------------------------------------------------------------------- | :---------------- | :--------------------------------------------------------------------------------------------- |
| Virtual Machines, Managed Disks, Public IP Addresses, empty App Service Plans | Retail Prices API | Reported as Estimated monthly cost at risk (custom). |
| Azure Advisor Cost recommendations | Azure Advisor | Reported separately as Estimated monthly savings (advisor) when Advisor returns an estimate. |
Generic Checks
All resources are also checked for:
- Missing tags: Resources without tags are flagged as potentially unmanaged
- Location mismatch: Resources not in the preferred location are reported
Prerequisites
- Node.js: Version 22 or higher.
- Azure Credentials: The library uses
DefaultAzureCredentialfrom@azure/identity, which supports various authentication methods:- Azure CLI (
az login) - Managed Identity
- Environment variables
- Visual Studio Code
- And more...
- Azure CLI (
Usage
Quick Start
import { azure, loadConfig } from "@pagopa/dx-savemoney";
// Load configuration (from file, env vars, or interactive prompt)
const config = await loadConfig("./config.yaml");
// Run analysis, then generate report
const reports = await azure.analyzeAzureResources(config);
await azure.generateReport(reports, "table");Configuration Inputs
The tool requires the following configuration:
| Input | Type | Required | Default | Description |
| :------------------ | :----------------------- | :------: | :----------- | :------------------------------------------------------------------------ |
| subscriptionIds | string[] | ✅ | - | Array of Azure subscription IDs to analyze |
| preferredLocation | string | ❌ | italynorth | Preferred Azure region (resources elsewhere will be flagged) |
| timespanDays | number | ❌ | 30 | Number of days to look back for metrics analysis |
| sources | FindingSource[] | ❌ | all sources | Restrict analysis to specific sources: "advisor", "custom", or both |
| verbose | boolean | ❌ | false | Enable detailed logging for each resource analyzed |
| filterTags | Record<string, string> | ❌ | - | Only analyze resources matching all the specified tag key-value pairs |
| thresholds | Thresholds | ❌ | see below | Override the default numeric thresholds used during analysis |
| azqrReportPath | string | ❌ | - | Path to an AZQR --json report to ingest (see AZQR Report Ingestion) |
Output Formats
The tool supports multiple output formats for different use cases:
| Format | Description | Use Case |
| :-------------- | :-------------------------------------------------- | :----------------------------- |
| table | Human-readable table in terminal | Quick visual inspection |
| json | Structured JSON with resource summaries | Integration with other tools |
| detailed-json | Complete JSON with full Azure resource metadata | AI analysis or deep inspection |
| lint | Linter-style output grouped by resource, with icons | CI pipelines and quick triage |
How to Load Configuration
The loadConfig() function loads configuration in the following priority order:
- Configuration file (pass file path as parameter)
- Environment variable (
ARM_SUBSCRIPTION_ID) - Interactive prompt (if no other configuration is found)
Example:
// From file
const config1 = await loadConfig("./config.yaml");
// From environment variable or prompt
const config2 = await loadConfig();Configuration File Example
azure:
subscriptionIds:
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
preferredLocation: italynorth
timespanDays: 30
verbose: false
thresholds: # optional — omit to use built-in defaults
vm:
cpuPercent: 5
storage:
transactionsPerDay: 50Thresholds Configuration
All numeric thresholds used during analysis can be overridden by adding a
thresholds section inside the azure block of your config YAML.
Only the fields you want to change are required — all others keep their
built-in defaults.
Example (full override, config.yaml):
azure:
subscriptionIds:
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
thresholds:
vm:
cpuPercent: 5
networkInBytesPerDay: 10485760
appService:
cpuPercent: 10
memoryPercent: 20
premiumCpuPercent: 15
containerApp:
cpuNanoCores: 5000000
memoryBytes: 52428800
networkBytes: 100000
storage:
transactionsPerDay: 50
publicIp:
bytesInDDoS: 1048576
staticSite:
siteHits: 500
bytesSent: 5242880Default threshold values:
| Resource | Field | Default Value | Description |
| :------------- | :--------------------- | :---------------------- | :-------------------------------------------- |
| vm | cpuPercent | 1 (%) | Average CPU below which VM is flagged |
| vm | networkInBytesPerDay | 3145728 (3 MB) | Average inbound traffic below which flagged |
| appService | cpuPercent | 5 (%) | Average CPU below which plan is flagged |
| appService | memoryPercent | 10 (%) | Average memory below which plan is flagged |
| appService | premiumCpuPercent | 10 (%) | CPU threshold for Premium-tier over-provision |
| containerApp | cpuNanoCores | 1000000 (0.001 cores) | Average CPU below which app is flagged |
| containerApp | memoryBytes | 10485760 (10 MB) | Average memory below which app is flagged |
| containerApp | networkBytes | 34000 (~33 KB) | Combined Rx+Tx below which app is flagged |
| storage | transactionsPerDay | 10 | Avg daily transactions below which flagged |
| publicIp | bytesInDDoS | 340000 (~332 KB) | Avg inbound bytes/day below which flagged |
| staticSite | siteHits | 100 | Total requests below which site is flagged |
| staticSite | bytesSent | 1048576 (1 MB) | Total bytes sent below which site is flagged |
Usage Examples
Tag Filtering
import { azure, loadConfig } from "@pagopa/dx-savemoney";
const config = await loadConfig("./config.yaml");
// Analyze only resources tagged with environment=prod AND team=platform
const reports = await azure.analyzeAzureResources({
...config,
filterTags: new Map([
["environment", "prod"],
["team", "platform"],
]),
});
await azure.generateReport(reports, "lint");Basic Usage
import { azure, loadConfig } from "@pagopa/dx-savemoney";
// Load from config file
const config = await loadConfig("./config.yaml");
const reports = await azure.analyzeAzureResources(config);
await azure.generateReport(reports, "table");Custom Configuration
import { azure } from "@pagopa/dx-savemoney";
import type { AzureConfig } from "@pagopa/dx-savemoney";
const config: AzureConfig = {
subscriptionIds: ["sub-id-1", "sub-id-2"],
preferredLocation: "italynorth",
timespanDays: 30,
verbose: true,
};
const reports = await azure.analyzeAzureResources(config);
await azure.generateReport(reports, "json");Generate Detailed Report
import { azure, loadConfig } from "@pagopa/dx-savemoney";
const config = await loadConfig();
// Generate detailed JSON with full resource metadata
const reports = await azure.analyzeAzureResources(config);
await azure.generateReport(reports, "detailed-json");Using Environment Variables
import { loadConfig, azure } from "@pagopa/dx-savemoney";
// Set environment variables
// ARM_SUBSCRIPTION_ID=sub1,sub2
const config = await loadConfig(); // Will read from env vars
const reports = await azure.analyzeAzureResources(config);
await azure.generateReport(reports, "json");AZQR Report Ingestion
SaveMoney can enrich a live scan with an AZQR
(azqr scan --json) report. Provide its path either with the --azqr-report
<path> CLI flag or via the azure.azqrReportPath YAML key (the CLI flag takes
precedence when both are set): the FinOps-relevant resources from the report's
impacted section are merged into the normal output as findings tagged with the
[azqr] source.
# Generate the AZQR report first (disable masking so resource IDs match)
azqr scan --subscription-id <sub-id> --json --mask=false
# Feed it into SaveMoney alongside the live scan
dx savemoney --azqr-report ./azqr_action_plan.jsonOr persist the path in the YAML config:
azure:
subscriptionIds:
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
azqrReportPath: ./azqr_action_plan.jsonNotes:
- Disable masking. AZQR masks subscription IDs by default
(
/subscriptions/xxxxxxxx-…/). Masked IDs cannot be matched to live resources, so findings would appear as separate stub entries. SaveMoney logs a warning when it detects a masked report; re-run AZQR with--mask=false. - Noise reduction. Rows are classified into two buckets and everything else
(security, reliability, high-availability best practices) is dropped:
- Cost opportunities (
costcategory): rows AZQR categorises asCost, plus orphaned/unassociated billable resources (public IPs, NAT gateways, application gateways, App Service plans, SQL elastic pools, load balancers, DDoS plans, VNet gateways, managed disks/snapshots). - Cleanup candidates (
operationalExcellencecategory, low severity): orphaned free resources flagged by AZQR'sAOR(Azure Orphan Resources) checks — empty subnets, unattached NSGs, orphan API connections, unconnected private endpoints. They carry no direct cost but are worth removing after verifying they are unused.
- Cost opportunities (
- AZQR carries no monetary estimate, so these findings have no per-finding saving amount; they surface as opportunities to review.
AWS (Coming Soon)
AWS support is planned for future releases with similar capabilities:
- Multi-account analysis
- Resource-specific detection algorithms
- Flexible reporting formats
- AWS-specific configuration options
The API will follow a similar pattern:
import { aws, loadAwsConfig } from "@pagopa/dx-savemoney";
const config = await loadAwsConfig("./aws-config.json");
await aws.analyzeAwsResources(config, "table");Development
Type Checking
pnpm typecheckLinting
pnpm lint # Auto-fix issues
pnpm lint:check # Check without fixingTesting
pnpm test # Run tests
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage reportFormatting
pnpm format # Format code
pnpm format:check # Check formatting