npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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:#fff

Installation

npm install @pagopa/dx-savemoney
# or
pnpm add @pagopa/dx-savemoney
# or
yarn add @pagopa/dx-savemoney

Azure

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 thresholds section 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

  1. Node.js: Version 22 or higher.
  2. Azure Credentials: The library uses DefaultAzureCredential from @azure/identity, which supports various authentication methods:
    • Azure CLI (az login)
    • Managed Identity
    • Environment variables
    • Visual Studio Code
    • And more...

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:

  1. Configuration file (pass file path as parameter)
  2. Environment variable (ARM_SUBSCRIPTION_ID)
  3. 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: 50

Thresholds 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: 5242880

Default 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.json

Or persist the path in the YAML config:

azure:
  subscriptionIds:
    - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  azqrReportPath: ./azqr_action_plan.json

Notes:

  • 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 (cost category): rows AZQR categorises as Cost, 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 (operationalExcellence category, low severity): orphaned free resources flagged by AZQR's AOR (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.
  • 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 typecheck

Linting

pnpm lint        # Auto-fix issues
pnpm lint:check  # Check without fixing

Testing

pnpm test              # Run tests
pnpm test:watch        # Watch mode
pnpm test:coverage     # With coverage report

Formatting

pnpm format        # Format code
pnpm format:check  # Check formatting