@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.
Files changed (83) hide show
  1. package/README.md +233 -0
  2. package/dist/azure/analyzer.d.ts +32 -0
  3. package/dist/azure/analyzer.d.ts.map +1 -0
  4. package/dist/azure/analyzer.js +128 -0
  5. package/dist/azure/analyzer.js.map +1 -0
  6. package/dist/azure/config.d.ts +19 -0
  7. package/dist/azure/config.d.ts.map +1 -0
  8. package/dist/azure/config.js +62 -0
  9. package/dist/azure/config.js.map +1 -0
  10. package/dist/azure/index.d.ts +9 -0
  11. package/dist/azure/index.d.ts.map +1 -0
  12. package/dist/azure/index.js +9 -0
  13. package/dist/azure/index.js.map +1 -0
  14. package/dist/azure/report.d.ts +12 -0
  15. package/dist/azure/report.d.ts.map +1 -0
  16. package/dist/azure/report.js +40 -0
  17. package/dist/azure/report.js.map +1 -0
  18. package/dist/azure/resources/app-service.d.ts +18 -0
  19. package/dist/azure/resources/app-service.d.ts.map +1 -0
  20. package/dist/azure/resources/app-service.js +65 -0
  21. package/dist/azure/resources/app-service.js.map +1 -0
  22. package/dist/azure/resources/disk.d.ts +16 -0
  23. package/dist/azure/resources/disk.d.ts.map +1 -0
  24. package/dist/azure/resources/disk.js +56 -0
  25. package/dist/azure/resources/disk.js.map +1 -0
  26. package/dist/azure/resources/index.d.ts +11 -0
  27. package/dist/azure/resources/index.d.ts.map +1 -0
  28. package/dist/azure/resources/index.js +11 -0
  29. package/dist/azure/resources/index.js.map +1 -0
  30. package/dist/azure/resources/nic.d.ts +15 -0
  31. package/dist/azure/resources/nic.d.ts.map +1 -0
  32. package/dist/azure/resources/nic.js +56 -0
  33. package/dist/azure/resources/nic.js.map +1 -0
  34. package/dist/azure/resources/private-endpoint.d.ts +15 -0
  35. package/dist/azure/resources/private-endpoint.d.ts.map +1 -0
  36. package/dist/azure/resources/private-endpoint.js +66 -0
  37. package/dist/azure/resources/private-endpoint.js.map +1 -0
  38. package/dist/azure/resources/public-ip.d.ts +18 -0
  39. package/dist/azure/resources/public-ip.d.ts.map +1 -0
  40. package/dist/azure/resources/public-ip.js +61 -0
  41. package/dist/azure/resources/public-ip.js.map +1 -0
  42. package/dist/azure/resources/storage.d.ts +16 -0
  43. package/dist/azure/resources/storage.d.ts.map +1 -0
  44. package/dist/azure/resources/storage.js +39 -0
  45. package/dist/azure/resources/storage.js.map +1 -0
  46. package/dist/azure/resources/vm.d.ts +19 -0
  47. package/dist/azure/resources/vm.d.ts.map +1 -0
  48. package/dist/azure/resources/vm.js +77 -0
  49. package/dist/azure/resources/vm.js.map +1 -0
  50. package/dist/azure/types.d.ts +34 -0
  51. package/dist/azure/types.d.ts.map +1 -0
  52. package/dist/azure/types.js +5 -0
  53. package/dist/azure/types.js.map +1 -0
  54. package/dist/azure/utils.d.ts +40 -0
  55. package/dist/azure/utils.d.ts.map +1 -0
  56. package/dist/azure/utils.js +104 -0
  57. package/dist/azure/utils.js.map +1 -0
  58. package/dist/index.d.ts +34 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +77 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/types.d.ts +21 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +14 -0
  65. package/dist/types.js.map +1 -0
  66. package/package.json +60 -0
  67. package/src/azure/analyzer.ts +213 -0
  68. package/src/azure/config.ts +81 -0
  69. package/src/azure/index.ts +9 -0
  70. package/src/azure/report.ts +52 -0
  71. package/src/azure/resources/app-service.ts +118 -0
  72. package/src/azure/resources/disk.ts +88 -0
  73. package/src/azure/resources/index.ts +11 -0
  74. package/src/azure/resources/nic.ts +90 -0
  75. package/src/azure/resources/private-endpoint.ts +112 -0
  76. package/src/azure/resources/public-ip.ts +106 -0
  77. package/src/azure/resources/storage.ts +67 -0
  78. package/src/azure/resources/vm.ts +129 -0
  79. package/src/azure/types.ts +38 -0
  80. package/src/azure/utils.ts +141 -0
  81. package/src/index.test.ts +95 -0
  82. package/src/index.ts +99 -0
  83. 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,5 @@
1
+ /**
2
+ * Azure-specific types
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -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":"AAAA;;GAEG;AAIH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7C;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,aAA4B,EAC5B,UAAkB,EAClB,UAAkB,EAClB,WAAmB,EACnB,YAAoB;IAEpB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,YAAY,GAAG,CAAC;QACrC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE;YAC1D,WAAW;YACX,WAAW,EAAE,UAAU;YACvB,QAAQ;SACT,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,gBAAgB,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;QAEnD,IACE,gBAAgB,KAAK,SAAS;YAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;YACD,OAAO,UAAU,CAAC,OAAO,CAAC;QAC5B,CAAC;QACD,IAAI,gBAAgB,KAAK,OAAO,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACzE,OAAO,UAAU,CAAC,KAAK,CAAC;QAC1B,CAAC;QACD,IACE,gBAAgB,KAAK,SAAS;YAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;YACD,OAAO,UAAU,CAAC,OAAO,CAAC;QAC5B,CAAC;QACD,IACE,gBAAgB,KAAK,SAAS;YAC9B,OAAO,UAAU,CAAC,OAAO,KAAK,QAAQ,EACtC,CAAC;YACD,OAAO,UAAU,CAAC,OAAO,CAAC;QAC5B,CAAC;QACD,IAAI,gBAAgB,KAAK,OAAO,IAAI,OAAO,UAAU,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YACzE,OAAO,UAAU,CAAC,KAAK,CAAC;QAC1B,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CACV,0BAA0B,UAAU,iBAAiB,UAAU,KAAK,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAC7H,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,OAAgB,EAChB,OAAe,EACf,MAAgB;IAEhB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;QAChE,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACtC,OAAgB,EAChB,MAAsB;IAEtB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;QACtC,MAAM,CAAC,KAAK,CAAC,iBAAiB,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,CAAC,KAAK,CACV,wBAAwB,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAChE,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,cAAc,MAAM,CAAC,MAAM,IAAI,iBAAiB,EAAE,CAAC,CAAC;QACjE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IACtC,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAAgB,EAChB,YAAoB,EACpB,YAAoB;IAEpB,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC5D,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;QACpC,MAAM,CAAC,KAAK,CAAC,iBAAiB,YAAY,EAAE,CAAC,CAAC;QAC9C,MAAM,CAAC,KAAK,CAAC,YAAY,YAAY,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/B,CAAC;AACH,CAAC"}
@@ -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"}
@@ -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
+ }