@pagopa/dx-savemoney 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/dist/azure/analyzer.d.ts +32 -0
- package/dist/azure/analyzer.d.ts.map +1 -0
- package/dist/azure/analyzer.js +128 -0
- package/dist/azure/analyzer.js.map +1 -0
- package/dist/azure/config.d.ts +19 -0
- package/dist/azure/config.d.ts.map +1 -0
- package/dist/azure/config.js +62 -0
- package/dist/azure/config.js.map +1 -0
- package/dist/azure/index.d.ts +9 -0
- package/dist/azure/index.d.ts.map +1 -0
- package/dist/azure/index.js +9 -0
- package/dist/azure/index.js.map +1 -0
- package/dist/azure/report.d.ts +12 -0
- package/dist/azure/report.d.ts.map +1 -0
- package/dist/azure/report.js +40 -0
- package/dist/azure/report.js.map +1 -0
- package/dist/azure/resources/app-service.d.ts +18 -0
- package/dist/azure/resources/app-service.d.ts.map +1 -0
- package/dist/azure/resources/app-service.js +65 -0
- package/dist/azure/resources/app-service.js.map +1 -0
- package/dist/azure/resources/disk.d.ts +16 -0
- package/dist/azure/resources/disk.d.ts.map +1 -0
- package/dist/azure/resources/disk.js +56 -0
- package/dist/azure/resources/disk.js.map +1 -0
- package/dist/azure/resources/index.d.ts +11 -0
- package/dist/azure/resources/index.d.ts.map +1 -0
- package/dist/azure/resources/index.js +11 -0
- package/dist/azure/resources/index.js.map +1 -0
- package/dist/azure/resources/nic.d.ts +15 -0
- package/dist/azure/resources/nic.d.ts.map +1 -0
- package/dist/azure/resources/nic.js +56 -0
- package/dist/azure/resources/nic.js.map +1 -0
- package/dist/azure/resources/private-endpoint.d.ts +15 -0
- package/dist/azure/resources/private-endpoint.d.ts.map +1 -0
- package/dist/azure/resources/private-endpoint.js +66 -0
- package/dist/azure/resources/private-endpoint.js.map +1 -0
- package/dist/azure/resources/public-ip.d.ts +18 -0
- package/dist/azure/resources/public-ip.d.ts.map +1 -0
- package/dist/azure/resources/public-ip.js +61 -0
- package/dist/azure/resources/public-ip.js.map +1 -0
- package/dist/azure/resources/storage.d.ts +16 -0
- package/dist/azure/resources/storage.d.ts.map +1 -0
- package/dist/azure/resources/storage.js +39 -0
- package/dist/azure/resources/storage.js.map +1 -0
- package/dist/azure/resources/vm.d.ts +19 -0
- package/dist/azure/resources/vm.d.ts.map +1 -0
- package/dist/azure/resources/vm.js +77 -0
- package/dist/azure/resources/vm.js.map +1 -0
- package/dist/azure/types.d.ts +34 -0
- package/dist/azure/types.d.ts.map +1 -0
- package/dist/azure/types.js +5 -0
- package/dist/azure/types.js.map +1 -0
- package/dist/azure/utils.d.ts +40 -0
- package/dist/azure/utils.d.ts.map +1 -0
- package/dist/azure/utils.js +104 -0
- package/dist/azure/utils.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +60 -0
- package/src/azure/analyzer.ts +213 -0
- package/src/azure/config.ts +81 -0
- package/src/azure/index.ts +9 -0
- package/src/azure/report.ts +52 -0
- package/src/azure/resources/app-service.ts +118 -0
- package/src/azure/resources/disk.ts +88 -0
- package/src/azure/resources/index.ts +11 -0
- package/src/azure/resources/nic.ts +90 -0
- package/src/azure/resources/private-endpoint.ts +112 -0
- package/src/azure/resources/public-ip.ts +106 -0
- package/src/azure/resources/storage.ts +67 -0
- package/src/azure/resources/vm.ts +129 -0
- package/src/azure/types.ts +38 -0
- package/src/azure/utils.ts +141 -0
- package/src/index.test.ts +95 -0
- package/src/index.ts +99 -0
- package/src/types.ts +34 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure-specific types
|
|
3
|
+
*/
|
|
4
|
+
import type * as armResources from "@azure/arm-resources";
|
|
5
|
+
import type { AnalysisResult, BaseConfig, CostRisk } from "../types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Azure configuration extending base config
|
|
8
|
+
*/
|
|
9
|
+
export type AzureConfig = BaseConfig & {
|
|
10
|
+
subscriptionIds: string[];
|
|
11
|
+
tenantId: string;
|
|
12
|
+
verbose?: boolean;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Detailed report for a single Azure resource with full resource object
|
|
16
|
+
*/
|
|
17
|
+
export type AzureDetailedResourceReport = {
|
|
18
|
+
analysis: AnalysisResult;
|
|
19
|
+
resource: armResources.GenericResource;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Summary report for a single Azure resource
|
|
23
|
+
*/
|
|
24
|
+
export type AzureResourceReport = {
|
|
25
|
+
costRisk: CostRisk;
|
|
26
|
+
location?: string;
|
|
27
|
+
name: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
resourceGroup?: string;
|
|
30
|
+
subscriptionId: string;
|
|
31
|
+
suspectedUnused: boolean;
|
|
32
|
+
type: string;
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAExE;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IACrC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,YAAY,CAAC,eAAe,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure utility functions for debugging and metrics
|
|
3
|
+
*/
|
|
4
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
5
|
+
import type { AnalysisResult } from "../types.js";
|
|
6
|
+
/**
|
|
7
|
+
* Fetches a specific metric for a resource from Azure Monitor.
|
|
8
|
+
*
|
|
9
|
+
* @param monitorClient - The Azure Monitor client instance
|
|
10
|
+
* @param resourceId - The Azure resource ID
|
|
11
|
+
* @param metricName - The name of the metric to fetch (e.g., "Percentage CPU")
|
|
12
|
+
* @param aggregation - The aggregation type (e.g., "Average", "Total")
|
|
13
|
+
* @param timespanDays - Number of days to look back for metrics
|
|
14
|
+
* @returns The metric value or null if unavailable
|
|
15
|
+
*/
|
|
16
|
+
export declare function getMetric(monitorClient: MonitorClient, resourceId: string, metricName: string, aggregation: string, timespanDays: number): Promise<null | number>;
|
|
17
|
+
/**
|
|
18
|
+
* Logs a verbose message, optionally with an object.
|
|
19
|
+
*
|
|
20
|
+
* @param verbose - Whether verbose logging is enabled
|
|
21
|
+
* @param message - The message to log
|
|
22
|
+
* @param object - Optional object to stringify and log
|
|
23
|
+
*/
|
|
24
|
+
export declare function verboseLog(verbose: boolean, message: string, object?: unknown): void;
|
|
25
|
+
/**
|
|
26
|
+
* Logs the analysis result for a resource.
|
|
27
|
+
*
|
|
28
|
+
* @param verbose - Whether verbose logging is enabled
|
|
29
|
+
* @param result - The analysis result object
|
|
30
|
+
*/
|
|
31
|
+
export declare function verboseLogAnalysisResult(verbose: boolean, result: AnalysisResult): void;
|
|
32
|
+
/**
|
|
33
|
+
* Logs a resource analysis header with visual separator.
|
|
34
|
+
*
|
|
35
|
+
* @param verbose - Whether verbose logging is enabled
|
|
36
|
+
* @param resourceName - Name of the resource being analyzed
|
|
37
|
+
* @param resourceType - Type of the resource
|
|
38
|
+
*/
|
|
39
|
+
export declare function verboseLogResourceStart(verbose: boolean, resourceName: string, resourceType: string): void;
|
|
40
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD;;;;;;;;;GASG;AACH,wBAAsB,SAAS,CAC7B,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC,CAiDxB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,OAAO,QAUjB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,cAAc,QAYvB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM,QASrB"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure utility functions for debugging and metrics
|
|
3
|
+
*/
|
|
4
|
+
import { getLogger } from "@logtape/logtape";
|
|
5
|
+
/**
|
|
6
|
+
* Fetches a specific metric for a resource from Azure Monitor.
|
|
7
|
+
*
|
|
8
|
+
* @param monitorClient - The Azure Monitor client instance
|
|
9
|
+
* @param resourceId - The Azure resource ID
|
|
10
|
+
* @param metricName - The name of the metric to fetch (e.g., "Percentage CPU")
|
|
11
|
+
* @param aggregation - The aggregation type (e.g., "Average", "Total")
|
|
12
|
+
* @param timespanDays - Number of days to look back for metrics
|
|
13
|
+
* @returns The metric value or null if unavailable
|
|
14
|
+
*/
|
|
15
|
+
export async function getMetric(monitorClient, resourceId, metricName, aggregation, timespanDays) {
|
|
16
|
+
try {
|
|
17
|
+
const timespan = `P${timespanDays}D`;
|
|
18
|
+
const result = await monitorClient.metrics.list(resourceId, {
|
|
19
|
+
aggregation,
|
|
20
|
+
metricnames: metricName,
|
|
21
|
+
timespan,
|
|
22
|
+
});
|
|
23
|
+
const metricData = result.value[0]?.timeseries?.[0]?.data?.[0];
|
|
24
|
+
if (!metricData) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const aggregationLower = aggregation.toLowerCase();
|
|
28
|
+
if (aggregationLower === "average" &&
|
|
29
|
+
typeof metricData.average === "number") {
|
|
30
|
+
return metricData.average;
|
|
31
|
+
}
|
|
32
|
+
if (aggregationLower === "total" && typeof metricData.total === "number") {
|
|
33
|
+
return metricData.total;
|
|
34
|
+
}
|
|
35
|
+
if (aggregationLower === "minimum" &&
|
|
36
|
+
typeof metricData.minimum === "number") {
|
|
37
|
+
return metricData.minimum;
|
|
38
|
+
}
|
|
39
|
+
if (aggregationLower === "maximum" &&
|
|
40
|
+
typeof metricData.maximum === "number") {
|
|
41
|
+
return metricData.maximum;
|
|
42
|
+
}
|
|
43
|
+
if (aggregationLower === "count" && typeof metricData.count === "number") {
|
|
44
|
+
return metricData.count;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const logger = getLogger(["savemoney", "azure", "metrics"]);
|
|
50
|
+
logger.error(`Failed to fetch metric ${metricName} for resource ${resourceId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Logs a verbose message, optionally with an object.
|
|
56
|
+
*
|
|
57
|
+
* @param verbose - Whether verbose logging is enabled
|
|
58
|
+
* @param message - The message to log
|
|
59
|
+
* @param object - Optional object to stringify and log
|
|
60
|
+
*/
|
|
61
|
+
export function verboseLog(verbose, message, object) {
|
|
62
|
+
if (verbose) {
|
|
63
|
+
const logger = getLogger(["savemoney", "azure", "verbose"]);
|
|
64
|
+
if (object !== undefined) {
|
|
65
|
+
logger.debug(`${message} ${JSON.stringify(object, null, 2)}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
logger.debug(message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Logs the analysis result for a resource.
|
|
74
|
+
*
|
|
75
|
+
* @param verbose - Whether verbose logging is enabled
|
|
76
|
+
* @param result - The analysis result object
|
|
77
|
+
*/
|
|
78
|
+
export function verboseLogAnalysisResult(verbose, result) {
|
|
79
|
+
if (verbose) {
|
|
80
|
+
const logger = getLogger(["savemoney", "azure", "verbose"]);
|
|
81
|
+
logger.debug("\n📊 ANALYSIS RESULT:");
|
|
82
|
+
logger.debug(` Cost Risk: ${result.costRisk.toUpperCase()}`);
|
|
83
|
+
logger.debug(` Suspected Unused: ${result.suspectedUnused ? "YES" : "NO"}`);
|
|
84
|
+
logger.debug(` Reason: ${result.reason || "No issues found"}`);
|
|
85
|
+
logger.debug("=".repeat(80) + "\n");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Logs a resource analysis header with visual separator.
|
|
90
|
+
*
|
|
91
|
+
* @param verbose - Whether verbose logging is enabled
|
|
92
|
+
* @param resourceName - Name of the resource being analyzed
|
|
93
|
+
* @param resourceType - Type of the resource
|
|
94
|
+
*/
|
|
95
|
+
export function verboseLogResourceStart(verbose, resourceName, resourceType) {
|
|
96
|
+
if (verbose) {
|
|
97
|
+
const logger = getLogger(["savemoney", "azure", "verbose"]);
|
|
98
|
+
logger.debug("\n" + "=".repeat(80));
|
|
99
|
+
logger.debug(`🔍 ANALYZING: ${resourceName}`);
|
|
100
|
+
logger.debug(` Type: ${resourceType}`);
|
|
101
|
+
logger.debug("=".repeat(80));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/azure/utils.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SaveMoney Package
|
|
3
|
+
*
|
|
4
|
+
* A tool that analyzes cloud resources and reports potentially unused
|
|
5
|
+
* or cost-inefficient ones.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multi-cloud support (Azure, AWS planned)
|
|
9
|
+
* - Metric-based analysis
|
|
10
|
+
* - Multiple output formats (table, JSON, detailed-JSON)
|
|
11
|
+
* - Configurable via CLI options, environment variables, or config file
|
|
12
|
+
*
|
|
13
|
+
* This tool does NOT modify, tag, or delete any resources.
|
|
14
|
+
*/
|
|
15
|
+
export type { AzureConfig } from "./azure/types.js";
|
|
16
|
+
export * from "./types.js";
|
|
17
|
+
import * as azureModule from "./azure/index.js";
|
|
18
|
+
export declare const azure: typeof azureModule;
|
|
19
|
+
import type { AzureConfig } from "./azure/types.js";
|
|
20
|
+
/**
|
|
21
|
+
* Loads configuration from file, environment variables, or interactive prompts.
|
|
22
|
+
*
|
|
23
|
+
* @param configPath - Optional path to JSON configuration file
|
|
24
|
+
* @returns Configuration object with subscription IDs and settings
|
|
25
|
+
*/
|
|
26
|
+
export declare function loadConfig(configPath?: string): Promise<AzureConfig>;
|
|
27
|
+
/**
|
|
28
|
+
* Prompts user for input via stdin.
|
|
29
|
+
*
|
|
30
|
+
* @param question - The question to display to the user
|
|
31
|
+
* @returns User's input as a string
|
|
32
|
+
*/
|
|
33
|
+
export declare function prompt(question: string): Promise<string>;
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,cAAc,YAAY,CAAC;AAG3B,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,eAAO,MAAM,KAAK,oBAAc,CAAC;AAOjC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA2C1E;AAED;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9D"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SaveMoney Package
|
|
3
|
+
*
|
|
4
|
+
* A tool that analyzes cloud resources and reports potentially unused
|
|
5
|
+
* or cost-inefficient ones.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Multi-cloud support (Azure, AWS planned)
|
|
9
|
+
* - Metric-based analysis
|
|
10
|
+
* - Multiple output formats (table, JSON, detailed-JSON)
|
|
11
|
+
* - Configurable via CLI options, environment variables, or config file
|
|
12
|
+
*
|
|
13
|
+
* This tool does NOT modify, tag, or delete any resources.
|
|
14
|
+
*/
|
|
15
|
+
export * from "./types.js";
|
|
16
|
+
// Export Azure module
|
|
17
|
+
import * as azureModule from "./azure/index.js";
|
|
18
|
+
export const azure = azureModule;
|
|
19
|
+
import { getLogger } from "@logtape/logtape";
|
|
20
|
+
// Utility imports for loadConfig and prompt functions
|
|
21
|
+
import * as fs from "fs";
|
|
22
|
+
import * as readline from "readline";
|
|
23
|
+
/**
|
|
24
|
+
* Loads configuration from file, environment variables, or interactive prompts.
|
|
25
|
+
*
|
|
26
|
+
* @param configPath - Optional path to JSON configuration file
|
|
27
|
+
* @returns Configuration object with subscription IDs and settings
|
|
28
|
+
*/
|
|
29
|
+
export async function loadConfig(configPath) {
|
|
30
|
+
const logger = getLogger(["savemoney", "config"]);
|
|
31
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
34
|
+
const config = JSON.parse(configContent);
|
|
35
|
+
// Validate required fields
|
|
36
|
+
if (!config.tenantId || !config.subscriptionIds) {
|
|
37
|
+
throw new Error("Config file must contain 'tenantId' and 'subscriptionIds'");
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...config,
|
|
41
|
+
preferredLocation: config.preferredLocation || "italynorth",
|
|
42
|
+
timespanDays: config.timespanDays || 30,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error(`Failed to load config file: ${error instanceof Error ? error.message : error}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
logger.info("Configuration file not found. Checking environment variables...");
|
|
50
|
+
const tenantId = process.env.ARM_TENANT_ID || (await prompt("Enter Tenant ID: "));
|
|
51
|
+
const subscriptionIds = process.env.ARM_SUBSCRIPTION_ID
|
|
52
|
+
? process.env.ARM_SUBSCRIPTION_ID.split(",")
|
|
53
|
+
: (await prompt("Enter Subscription IDs (comma-separated): ")).split(",");
|
|
54
|
+
return {
|
|
55
|
+
preferredLocation: "italynorth",
|
|
56
|
+
subscriptionIds,
|
|
57
|
+
tenantId,
|
|
58
|
+
timespanDays: 30,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Prompts user for input via stdin.
|
|
63
|
+
*
|
|
64
|
+
* @param question - The question to display to the user
|
|
65
|
+
* @returns User's input as a string
|
|
66
|
+
*/
|
|
67
|
+
export async function prompt(question) {
|
|
68
|
+
const rl = readline.createInterface({
|
|
69
|
+
input: process.stdin,
|
|
70
|
+
output: process.stdout,
|
|
71
|
+
});
|
|
72
|
+
return new Promise((resolve) => rl.question(question, (answer) => {
|
|
73
|
+
rl.close();
|
|
74
|
+
resolve(answer);
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,cAAc,YAAY,CAAC;AAE3B,sBAAsB;AACtB,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,CAAC;AAEjC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,sDAAsD;AACtD,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AAIrC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAmB;IAClD,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC,CAAC;IAElD,IAAI,UAAU,IAAI,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5C,IAAI,CAAC;YACH,MAAM,aAAa,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;YAEzC,2BAA2B;YAC3B,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;gBAChD,MAAM,IAAI,KAAK,CACb,2DAA2D,CAC5D,CAAC;YACJ,CAAC;YAED,OAAO;gBACL,GAAG,MAAM;gBACT,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,IAAI,YAAY;gBAC3D,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,EAAE;aACxC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,+BAA+B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAChF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CACT,iEAAiE,CAClE,CAAC;IAEF,MAAM,QAAQ,GACZ,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,CAAC,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACnE,MAAM,eAAe,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB;QACrD,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,KAAK,CAAC,GAAG,CAAC;QAC5C,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,4CAA4C,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE5E,OAAO;QACL,iBAAiB,EAAE,YAAY;QAC/B,eAAe;QACf,QAAQ;QACR,YAAY,EAAE,EAAE;KACjB,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,MAAM,CAAC,QAAgB;IAC3C,MAAM,EAAE,GAAG,QAAQ,CAAC,eAAe,CAAC;QAClC,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAC7B,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,EAAE;QAC/B,EAAE,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,CAAC,MAAM,CAAC,CAAC;IAClB,CAAC,CAAC,CACH,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common types shared across all cloud providers
|
|
3
|
+
*/
|
|
4
|
+
export type AnalysisResult = {
|
|
5
|
+
costRisk: CostRisk;
|
|
6
|
+
reason: string;
|
|
7
|
+
suspectedUnused: boolean;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Base configuration interface that all cloud providers should extend
|
|
11
|
+
*/
|
|
12
|
+
export type BaseConfig = {
|
|
13
|
+
preferredLocation: string;
|
|
14
|
+
timespanDays: number;
|
|
15
|
+
};
|
|
16
|
+
export type CostRisk = "high" | "low" | "medium";
|
|
17
|
+
/**
|
|
18
|
+
* Merges analysis results, preserving existing reasons and combining suspectedUnused flags.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mergeResults(baseResult: AnalysisResult, specificResult: AnalysisResult): AnalysisResult;
|
|
21
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjD;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,cAAc,GAC7B,cAAc,CAOhB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common types shared across all cloud providers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Merges analysis results, preserving existing reasons and combining suspectedUnused flags.
|
|
6
|
+
*/
|
|
7
|
+
export function mergeResults(baseResult, specificResult) {
|
|
8
|
+
return {
|
|
9
|
+
costRisk: specificResult.costRisk,
|
|
10
|
+
reason: baseResult.reason + specificResult.reason,
|
|
11
|
+
suspectedUnused: baseResult.suspectedUnused || specificResult.suspectedUnused,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAkBH;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,UAA0B,EAC1B,cAA8B;IAE9B,OAAO;QACL,QAAQ,EAAE,cAAc,CAAC,QAAQ;QACjC,MAAM,EAAE,UAAU,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM;QACjD,eAAe,EACb,UAAU,CAAC,eAAe,IAAI,cAAc,CAAC,eAAe;KAC/D,CAAC;AACJ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pagopa/dx-savemoney",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Azure resource analyzer for finding unused or cost-inefficient resources.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/pagopa/dx.git",
|
|
9
|
+
"directory": "packages/savemoney"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src",
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"azure",
|
|
25
|
+
"typescript",
|
|
26
|
+
"finops",
|
|
27
|
+
"DX"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@azure/arm-appservice": "^17.0.0",
|
|
31
|
+
"@azure/arm-compute": "^23.1.0",
|
|
32
|
+
"@azure/arm-monitor": "^7.0.0",
|
|
33
|
+
"@azure/arm-network": "^34.1.0",
|
|
34
|
+
"@azure/arm-resources": "^7.0.0",
|
|
35
|
+
"@azure/identity": "^4.13.0",
|
|
36
|
+
"@logtape/logtape": "^1.1.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@tsconfig/node22": "22.0.2",
|
|
40
|
+
"@types/node": "^22.16.2",
|
|
41
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
42
|
+
"eslint": "^9.30.0",
|
|
43
|
+
"prettier": "3.6.2",
|
|
44
|
+
"typescript": "~5.8.3",
|
|
45
|
+
"vitest": "^3.2.4",
|
|
46
|
+
"@pagopa/eslint-config": "^5.1.0"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "rm -rf dist && tsc",
|
|
50
|
+
"dev": "tsc --watch",
|
|
51
|
+
"lint": "eslint --fix .",
|
|
52
|
+
"lint:check": "eslint .",
|
|
53
|
+
"format": "prettier --write .",
|
|
54
|
+
"format:check": "prettier --check .",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:coverage": "vitest run --coverage",
|
|
57
|
+
"test:watch": "vitest run --watch",
|
|
58
|
+
"typecheck": "tsc --noEmit"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure resource analyzer - Main orchestration logic
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
6
|
+
import { ComputeManagementClient } from "@azure/arm-compute";
|
|
7
|
+
import { MonitorClient } from "@azure/arm-monitor";
|
|
8
|
+
import { NetworkManagementClient } from "@azure/arm-network";
|
|
9
|
+
import * as armResources from "@azure/arm-resources";
|
|
10
|
+
import { DefaultAzureCredential } from "@azure/identity";
|
|
11
|
+
import { getLogger } from "@logtape/logtape";
|
|
12
|
+
|
|
13
|
+
import type { AzureConfig, AzureDetailedResourceReport } from "./types.js";
|
|
14
|
+
|
|
15
|
+
import { type AnalysisResult, mergeResults } from "../types.js";
|
|
16
|
+
import { generateReport } from "./report.js";
|
|
17
|
+
import {
|
|
18
|
+
analyzeAppServicePlan,
|
|
19
|
+
analyzeDisk,
|
|
20
|
+
analyzeNic,
|
|
21
|
+
analyzePrivateEndpoint,
|
|
22
|
+
analyzePublicIp,
|
|
23
|
+
analyzeStorageAccount,
|
|
24
|
+
analyzeVM,
|
|
25
|
+
} from "./resources/index.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Analyzes resources in multiple Azure subscriptions and generates a report.
|
|
29
|
+
*
|
|
30
|
+
* @param config - Azure configuration with subscription IDs and settings
|
|
31
|
+
* @param format - Output format (table, json, or detailed-json)
|
|
32
|
+
*/
|
|
33
|
+
export async function analyzeAzureResources(
|
|
34
|
+
config: AzureConfig,
|
|
35
|
+
format: "detailed-json" | "json" | "table",
|
|
36
|
+
) {
|
|
37
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
38
|
+
const credential = new DefaultAzureCredential();
|
|
39
|
+
const allReports: AzureDetailedResourceReport[] = [];
|
|
40
|
+
|
|
41
|
+
for (const subscriptionId of config.subscriptionIds) {
|
|
42
|
+
logger.info(`Analyzing subscription: ${subscriptionId}`);
|
|
43
|
+
|
|
44
|
+
const resourceClient = new armResources.ResourceManagementClient(
|
|
45
|
+
credential,
|
|
46
|
+
subscriptionId.trim(),
|
|
47
|
+
);
|
|
48
|
+
const monitorClient = new MonitorClient(credential, subscriptionId.trim());
|
|
49
|
+
const computeClient = new ComputeManagementClient(
|
|
50
|
+
credential,
|
|
51
|
+
subscriptionId.trim(),
|
|
52
|
+
);
|
|
53
|
+
const networkClient = new NetworkManagementClient(
|
|
54
|
+
credential,
|
|
55
|
+
subscriptionId.trim(),
|
|
56
|
+
);
|
|
57
|
+
const webSiteClient = new WebSiteManagementClient(
|
|
58
|
+
credential,
|
|
59
|
+
subscriptionId.trim(),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Use the async iterator to avoid memory explosion for large environments
|
|
63
|
+
for await (const resource of resourceClient.resources.list()) {
|
|
64
|
+
const { costRisk, reason, suspectedUnused } = await analyzeResource(
|
|
65
|
+
resource,
|
|
66
|
+
monitorClient,
|
|
67
|
+
computeClient,
|
|
68
|
+
networkClient,
|
|
69
|
+
webSiteClient,
|
|
70
|
+
config.preferredLocation,
|
|
71
|
+
config.timespanDays,
|
|
72
|
+
config.verbose || false,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (suspectedUnused) {
|
|
76
|
+
allReports.push({
|
|
77
|
+
analysis: {
|
|
78
|
+
costRisk,
|
|
79
|
+
reason: reason || "No specific findings.",
|
|
80
|
+
suspectedUnused,
|
|
81
|
+
},
|
|
82
|
+
resource: resource,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Sort to make the output more readable
|
|
89
|
+
allReports.sort((a, b) => {
|
|
90
|
+
if (a.analysis.costRisk === b.analysis.costRisk)
|
|
91
|
+
return (a.resource.name ?? "").localeCompare(b.resource.name ?? "");
|
|
92
|
+
const order = { high: 0, low: 2, medium: 1 };
|
|
93
|
+
return order[a.analysis.costRisk] - order[b.analysis.costRisk];
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await generateReport(allReports, format);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Analyzes a single Azure resource based on its type.
|
|
101
|
+
*
|
|
102
|
+
* @param resource - The Azure resource to analyze
|
|
103
|
+
* @param monitorClient - Azure Monitor client for metrics
|
|
104
|
+
* @param computeClient - Azure Compute client
|
|
105
|
+
* @param networkClient - Azure Network client
|
|
106
|
+
* @param webSiteClient - Azure Web Site client
|
|
107
|
+
* @param preferredLocation - Preferred Azure location
|
|
108
|
+
* @param timespanDays - Number of days to analyze metrics
|
|
109
|
+
* @param verbose - Whether verbose logging is enabled
|
|
110
|
+
* @returns Analysis result with cost risk and reason
|
|
111
|
+
*/
|
|
112
|
+
export async function analyzeResource(
|
|
113
|
+
resource: armResources.GenericResource,
|
|
114
|
+
monitorClient: MonitorClient,
|
|
115
|
+
computeClient: ComputeManagementClient,
|
|
116
|
+
networkClient: NetworkManagementClient,
|
|
117
|
+
webSiteClient: WebSiteManagementClient,
|
|
118
|
+
preferredLocation: string,
|
|
119
|
+
timespanDays: number,
|
|
120
|
+
verbose = false,
|
|
121
|
+
): Promise<AnalysisResult> {
|
|
122
|
+
const type = resource.type?.toLowerCase() || "";
|
|
123
|
+
let result = {
|
|
124
|
+
costRisk: "low" as "high" | "low" | "medium",
|
|
125
|
+
reason: "",
|
|
126
|
+
suspectedUnused: false,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Generic check: lack of tags is a common sign of unmanaged resources.
|
|
130
|
+
if (!resource.tags || Object.keys(resource.tags).length === 0) {
|
|
131
|
+
result.suspectedUnused = true;
|
|
132
|
+
result.reason += "No tags found. ";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Route to type-specific analysis hooks
|
|
136
|
+
switch (type) {
|
|
137
|
+
case "microsoft.compute/disks": {
|
|
138
|
+
const diskResult = await analyzeDisk(resource, computeClient, verbose);
|
|
139
|
+
result = mergeResults(result, diskResult);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case "microsoft.compute/virtualmachines": {
|
|
143
|
+
const vmResult = await analyzeVM(
|
|
144
|
+
resource,
|
|
145
|
+
monitorClient,
|
|
146
|
+
computeClient,
|
|
147
|
+
timespanDays,
|
|
148
|
+
verbose,
|
|
149
|
+
);
|
|
150
|
+
result = mergeResults(result, vmResult);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case "microsoft.network/networkinterfaces": {
|
|
154
|
+
const nicResult = await analyzeNic(resource, networkClient, verbose);
|
|
155
|
+
result = mergeResults(result, nicResult);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case "microsoft.network/privateendpoints": {
|
|
159
|
+
const peResult = await analyzePrivateEndpoint(
|
|
160
|
+
resource,
|
|
161
|
+
networkClient,
|
|
162
|
+
verbose,
|
|
163
|
+
);
|
|
164
|
+
result = mergeResults(result, peResult);
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case "microsoft.network/publicipaddresses": {
|
|
168
|
+
const pipResult = await analyzePublicIp(
|
|
169
|
+
resource,
|
|
170
|
+
networkClient,
|
|
171
|
+
monitorClient,
|
|
172
|
+
timespanDays,
|
|
173
|
+
verbose,
|
|
174
|
+
);
|
|
175
|
+
result = mergeResults(result, pipResult);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
case "microsoft.storage/storageaccounts": {
|
|
179
|
+
const storageResult = await analyzeStorageAccount(
|
|
180
|
+
resource,
|
|
181
|
+
monitorClient,
|
|
182
|
+
timespanDays,
|
|
183
|
+
verbose,
|
|
184
|
+
);
|
|
185
|
+
result = mergeResults(result, storageResult);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "microsoft.web/serverfarms": {
|
|
189
|
+
const aspResult = await analyzeAppServicePlan(
|
|
190
|
+
resource,
|
|
191
|
+
webSiteClient,
|
|
192
|
+
monitorClient,
|
|
193
|
+
timespanDays,
|
|
194
|
+
verbose,
|
|
195
|
+
);
|
|
196
|
+
result = mergeResults(result, aspResult);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
default:
|
|
200
|
+
result.reason += "No specific analysis for this resource type. ";
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Generic check for location
|
|
205
|
+
if (
|
|
206
|
+
resource.location &&
|
|
207
|
+
!resource.location.toLowerCase().includes(preferredLocation.toLowerCase())
|
|
208
|
+
) {
|
|
209
|
+
result.reason += `Resource not in preferred location (${preferredLocation}). `;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { ...result, reason: result.reason.trim() };
|
|
213
|
+
}
|