@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,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure configuration loading utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getLogger } from "@logtape/logtape";
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as readline from "readline";
|
|
8
|
+
|
|
9
|
+
import type { AzureConfig } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Loads Azure configuration from file, environment variables, or interactive prompts.
|
|
13
|
+
*
|
|
14
|
+
* @param configPath - Optional path to JSON configuration file
|
|
15
|
+
* @returns Azure configuration object with subscription IDs and settings
|
|
16
|
+
*/
|
|
17
|
+
export async function loadAzureConfig(
|
|
18
|
+
configPath?: string,
|
|
19
|
+
): Promise<AzureConfig> {
|
|
20
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
23
|
+
const config = JSON.parse(configContent) as Partial<AzureConfig>;
|
|
24
|
+
|
|
25
|
+
if (!config.tenantId || !config.subscriptionIds) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Config file must contain 'tenantId' and 'subscriptionIds'",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...config,
|
|
33
|
+
preferredLocation: config.preferredLocation || "italynorth",
|
|
34
|
+
subscriptionIds: config.subscriptionIds,
|
|
35
|
+
tenantId: config.tenantId,
|
|
36
|
+
timespanDays: config.timespanDays || 30,
|
|
37
|
+
};
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Failed to load config file: ${error instanceof Error ? error.message : error}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const logger = getLogger(["savemoney", "azure", "config"]);
|
|
46
|
+
logger.info(
|
|
47
|
+
"Configuration file not found. Checking environment variables...",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const tenantId =
|
|
51
|
+
process.env.ARM_TENANT_ID || (await prompt("Enter Tenant ID: "));
|
|
52
|
+
const subscriptionIds = process.env.ARM_SUBSCRIPTION_ID
|
|
53
|
+
? process.env.ARM_SUBSCRIPTION_ID.split(",")
|
|
54
|
+
: (await prompt("Enter Subscription IDs (comma-separated): ")).split(",");
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
preferredLocation: "italynorth",
|
|
58
|
+
subscriptionIds,
|
|
59
|
+
tenantId,
|
|
60
|
+
timespanDays: 30,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prompts user for input via stdin.
|
|
66
|
+
*
|
|
67
|
+
* @param question - The question to display to the user
|
|
68
|
+
* @returns User's input as a string
|
|
69
|
+
*/
|
|
70
|
+
export async function prompt(question: string): Promise<string> {
|
|
71
|
+
const rl = readline.createInterface({
|
|
72
|
+
input: process.stdin,
|
|
73
|
+
output: process.stdout,
|
|
74
|
+
});
|
|
75
|
+
return new Promise((resolve) =>
|
|
76
|
+
rl.question(question, (answer) => {
|
|
77
|
+
rl.close();
|
|
78
|
+
resolve(answer);
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure report generation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
AzureDetailedResourceReport,
|
|
7
|
+
AzureResourceReport,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generates a report from Azure resource analysis in the specified format.
|
|
12
|
+
*
|
|
13
|
+
* @param report - Array of detailed resource reports
|
|
14
|
+
* @param format - Output format (table, json, or detailed-json)
|
|
15
|
+
*/
|
|
16
|
+
export async function generateReport(
|
|
17
|
+
report: AzureDetailedResourceReport[],
|
|
18
|
+
format: "detailed-json" | "json" | "table",
|
|
19
|
+
) {
|
|
20
|
+
if (format === "detailed-json") {
|
|
21
|
+
console.log(JSON.stringify(report, null, 2));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// For other formats, we extract the summary data.
|
|
26
|
+
const summaryReport: AzureResourceReport[] = report.map((r) => ({
|
|
27
|
+
costRisk: r.analysis.costRisk,
|
|
28
|
+
location: r.resource.location ?? "",
|
|
29
|
+
name: r.resource.name ?? "unknown",
|
|
30
|
+
reason: r.analysis.reason,
|
|
31
|
+
resourceGroup: r.resource.id?.split("/")[4],
|
|
32
|
+
subscriptionId: r.resource.id?.split("/")[2] ?? "unknown",
|
|
33
|
+
suspectedUnused: r.analysis.suspectedUnused,
|
|
34
|
+
type: r.resource.type ?? "unknown",
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
if (format === "json") {
|
|
38
|
+
console.log(JSON.stringify(summaryReport, null, 2));
|
|
39
|
+
} else {
|
|
40
|
+
console.table(
|
|
41
|
+
summaryReport.map((r) => ({
|
|
42
|
+
Name: r.name,
|
|
43
|
+
Reason: r.reason,
|
|
44
|
+
"Resource Group": r.resourceGroup || "N/A",
|
|
45
|
+
Risk: r.costRisk,
|
|
46
|
+
Type: r.type,
|
|
47
|
+
Unused: r.suspectedUnused ? "Yes" : "No",
|
|
48
|
+
})),
|
|
49
|
+
["Name", "Type", "Resource Group", "Risk", "Unused", "Reason"],
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure App Service Plan analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
6
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
7
|
+
|
|
8
|
+
import * as armResources from "@azure/arm-resources";
|
|
9
|
+
import { getLogger } from "@logtape/logtape";
|
|
10
|
+
|
|
11
|
+
import type { AnalysisResult } from "../../types.js";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getMetric,
|
|
15
|
+
verboseLog,
|
|
16
|
+
verboseLogAnalysisResult,
|
|
17
|
+
verboseLogResourceStart,
|
|
18
|
+
} from "../utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Analyzes an Azure App Service Plan for potential cost optimization.
|
|
22
|
+
*
|
|
23
|
+
* @param resource - The Azure resource object
|
|
24
|
+
* @param webSiteClient - Azure Web Site client for App Service Plan details
|
|
25
|
+
* @param monitorClient - Azure Monitor client for metrics
|
|
26
|
+
* @param timespanDays - Number of days to analyze metrics
|
|
27
|
+
* @returns Analysis result with cost risk and reason
|
|
28
|
+
*/
|
|
29
|
+
export async function analyzeAppServicePlan(
|
|
30
|
+
resource: armResources.GenericResource,
|
|
31
|
+
webSiteClient: WebSiteManagementClient,
|
|
32
|
+
monitorClient: MonitorClient,
|
|
33
|
+
timespanDays: number,
|
|
34
|
+
verbose = false,
|
|
35
|
+
): Promise<AnalysisResult> {
|
|
36
|
+
verboseLogResourceStart(
|
|
37
|
+
verbose,
|
|
38
|
+
resource.name || "unknown",
|
|
39
|
+
"App Service Plan (microsoft.web/serverfarms)",
|
|
40
|
+
);
|
|
41
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
42
|
+
|
|
43
|
+
const costRisk: "high" | "low" | "medium" = "high";
|
|
44
|
+
let reason = "";
|
|
45
|
+
|
|
46
|
+
if (!resource.id) {
|
|
47
|
+
return {
|
|
48
|
+
costRisk,
|
|
49
|
+
reason: "Resource ID is missing.",
|
|
50
|
+
suspectedUnused: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract resource group and App Service Plan name from resource ID
|
|
55
|
+
const resourceParts = resource.id.split("/");
|
|
56
|
+
const resourceGroupName = resourceParts[4];
|
|
57
|
+
const planName = resourceParts[8];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Get detailed App Service Plan information
|
|
61
|
+
const planDetails = await webSiteClient.appServicePlans.get(
|
|
62
|
+
resourceGroupName,
|
|
63
|
+
planName,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
verboseLog(verbose, "App Service Plan API details:", planDetails);
|
|
67
|
+
|
|
68
|
+
// Check if the plan has no apps
|
|
69
|
+
if (!planDetails.numberOfSites || planDetails.numberOfSites === 0) {
|
|
70
|
+
reason += "App Service Plan has no apps deployed. ";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check CPU and Memory metrics
|
|
74
|
+
const cpuPercentage = await getMetric(
|
|
75
|
+
monitorClient,
|
|
76
|
+
resource.id,
|
|
77
|
+
"CpuPercentage",
|
|
78
|
+
"Average",
|
|
79
|
+
timespanDays,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const memoryPercentage = await getMetric(
|
|
83
|
+
monitorClient,
|
|
84
|
+
resource.id,
|
|
85
|
+
"MemoryPercentage",
|
|
86
|
+
"Average",
|
|
87
|
+
timespanDays,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (cpuPercentage !== null && cpuPercentage < 5) {
|
|
91
|
+
reason += `Very low CPU usage (${cpuPercentage.toFixed(2)}%). `;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (memoryPercentage !== null && memoryPercentage < 10) {
|
|
95
|
+
reason += `Very low memory usage (${memoryPercentage.toFixed(2)}%). `;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if it's an oversized plan (Premium tier with low usage)
|
|
99
|
+
if (
|
|
100
|
+
planDetails.sku?.tier?.includes("Premium") &&
|
|
101
|
+
cpuPercentage &&
|
|
102
|
+
cpuPercentage < 10
|
|
103
|
+
) {
|
|
104
|
+
reason += "Premium tier with low resource utilization. ";
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
108
|
+
logger.warn(
|
|
109
|
+
`Failed to get App Service Plan details for ${planName}: ${error instanceof Error ? error.message : error}`,
|
|
110
|
+
);
|
|
111
|
+
reason += "Could not retrieve detailed App Service Plan information. ";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const suspectedUnused = reason.length > 0;
|
|
115
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
116
|
+
verboseLogAnalysisResult(verbose, result);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Managed Disk analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ComputeManagementClient } from "@azure/arm-compute";
|
|
6
|
+
|
|
7
|
+
import * as armResources from "@azure/arm-resources";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
|
|
10
|
+
import type { AnalysisResult } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
verboseLog,
|
|
14
|
+
verboseLogAnalysisResult,
|
|
15
|
+
verboseLogResourceStart,
|
|
16
|
+
} from "../utils.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Analyzes an Azure Managed Disk for potential cost optimization.
|
|
20
|
+
*
|
|
21
|
+
* @param resource - The Azure resource object
|
|
22
|
+
* @param computeClient - Azure Compute client for disk details
|
|
23
|
+
* @param verbose - Whether verbose logging is enabled
|
|
24
|
+
* @returns Analysis result with cost risk and reason
|
|
25
|
+
*/
|
|
26
|
+
export async function analyzeDisk(
|
|
27
|
+
resource: armResources.GenericResource,
|
|
28
|
+
computeClient: ComputeManagementClient,
|
|
29
|
+
verbose = false,
|
|
30
|
+
): Promise<AnalysisResult> {
|
|
31
|
+
verboseLogResourceStart(
|
|
32
|
+
verbose,
|
|
33
|
+
resource.name || "unknown",
|
|
34
|
+
"Managed Disk (microsoft.compute/disks)",
|
|
35
|
+
);
|
|
36
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
37
|
+
|
|
38
|
+
const costRisk: "high" | "low" | "medium" = "medium";
|
|
39
|
+
|
|
40
|
+
if (!resource.id) {
|
|
41
|
+
return {
|
|
42
|
+
costRisk,
|
|
43
|
+
reason: "Resource ID is missing.",
|
|
44
|
+
suspectedUnused: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract resource group and disk name from resource ID
|
|
49
|
+
const resourceParts = resource.id.split("/");
|
|
50
|
+
const resourceGroupName = resourceParts[4];
|
|
51
|
+
const diskName = resourceParts[8];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Get the actual disk details to check attachment state
|
|
55
|
+
const diskDetails = await computeClient.disks.get(
|
|
56
|
+
resourceGroupName,
|
|
57
|
+
diskName,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
verboseLog(verbose, "Disk API details:", diskDetails);
|
|
61
|
+
|
|
62
|
+
// Check if disk is unattached
|
|
63
|
+
if (
|
|
64
|
+
diskDetails.diskState?.toLowerCase() === "unattached" ||
|
|
65
|
+
!diskDetails.managedBy
|
|
66
|
+
) {
|
|
67
|
+
const result = {
|
|
68
|
+
costRisk,
|
|
69
|
+
reason: "Disk is unattached. ",
|
|
70
|
+
suspectedUnused: true,
|
|
71
|
+
};
|
|
72
|
+
verboseLogAnalysisResult(verbose, result);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
77
|
+
logger.warn(
|
|
78
|
+
`Failed to get disk details for ${diskName}: ${error instanceof Error ? error.message : error}`,
|
|
79
|
+
);
|
|
80
|
+
// Fallback to checking properties if API call fails
|
|
81
|
+
// Note: GenericResource doesn't have diskState property
|
|
82
|
+
// Without API access, we can't determine if disk is unattached
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = { costRisk, reason: "", suspectedUnused: false };
|
|
86
|
+
verboseLogAnalysisResult(verbose, result);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure resource-specific analysis functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { analyzeAppServicePlan } from "./app-service.js";
|
|
6
|
+
export { analyzeDisk } from "./disk.js";
|
|
7
|
+
export { analyzeNic } from "./nic.js";
|
|
8
|
+
export { analyzePrivateEndpoint } from "./private-endpoint.js";
|
|
9
|
+
export { analyzePublicIp } from "./public-ip.js";
|
|
10
|
+
export { analyzeStorageAccount } from "./storage.js";
|
|
11
|
+
export { analyzeVM } from "./vm.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Network Interface analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NetworkManagementClient } from "@azure/arm-network";
|
|
6
|
+
|
|
7
|
+
import * as armResources from "@azure/arm-resources";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
|
|
10
|
+
import type { AnalysisResult } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
verboseLog,
|
|
14
|
+
verboseLogAnalysisResult,
|
|
15
|
+
verboseLogResourceStart,
|
|
16
|
+
} from "../utils.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Analyzes an Azure Network Interface for potential cost optimization.
|
|
20
|
+
*
|
|
21
|
+
* @param resource - The Azure resource object
|
|
22
|
+
* @param networkClient - Azure Network client for NIC details
|
|
23
|
+
* @returns Analysis result with cost risk and reason
|
|
24
|
+
*/
|
|
25
|
+
export async function analyzeNic(
|
|
26
|
+
resource: armResources.GenericResource,
|
|
27
|
+
networkClient: NetworkManagementClient,
|
|
28
|
+
verbose = false,
|
|
29
|
+
): Promise<AnalysisResult> {
|
|
30
|
+
verboseLogResourceStart(
|
|
31
|
+
verbose,
|
|
32
|
+
resource.name || "unknown",
|
|
33
|
+
"Network Interface (microsoft.network/networkinterfaces)",
|
|
34
|
+
);
|
|
35
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
36
|
+
|
|
37
|
+
const costRisk: "high" | "low" | "medium" = "medium";
|
|
38
|
+
let reason = "";
|
|
39
|
+
|
|
40
|
+
if (!resource.id) {
|
|
41
|
+
return {
|
|
42
|
+
costRisk,
|
|
43
|
+
reason: "Resource ID is missing.",
|
|
44
|
+
suspectedUnused: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract resource group and NIC name from resource ID
|
|
49
|
+
const resourceParts = resource.id.split("/");
|
|
50
|
+
const resourceGroupName = resourceParts[4];
|
|
51
|
+
const nicName = resourceParts[8];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Get detailed NIC information
|
|
55
|
+
const nicDetails = await networkClient.networkInterfaces.get(
|
|
56
|
+
resourceGroupName,
|
|
57
|
+
nicName,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
verboseLog(verbose, "NIC API details:", nicDetails);
|
|
61
|
+
|
|
62
|
+
// Check if NIC is not attached to any VM or private endpoint
|
|
63
|
+
if (!nicDetails.virtualMachine && !nicDetails.privateEndpoint) {
|
|
64
|
+
reason += "NIC not attached to any VM or private endpoint. ";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if NIC has no public IP
|
|
68
|
+
const hasPublicIP = nicDetails.ipConfigurations?.some(
|
|
69
|
+
(config) => config.publicIPAddress,
|
|
70
|
+
);
|
|
71
|
+
if (!hasPublicIP) {
|
|
72
|
+
reason += "No public IP assigned. ";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Note: Network Interface metrics are not available for Private Endpoint NICs
|
|
76
|
+
// and most Azure NICs don't expose standard traffic metrics through Azure Monitor
|
|
77
|
+
// The primary checks (attachment to VM and public IP assignment) are sufficient
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
80
|
+
logger.warn(
|
|
81
|
+
`Failed to get NIC details for ${nicName}: ${error instanceof Error ? error.message : error}`,
|
|
82
|
+
);
|
|
83
|
+
reason += "Could not retrieve detailed NIC information. ";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const suspectedUnused = reason.length > 0;
|
|
87
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
88
|
+
verboseLogAnalysisResult(verbose, result);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Private Endpoint analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NetworkManagementClient } from "@azure/arm-network";
|
|
6
|
+
|
|
7
|
+
import * as armResources from "@azure/arm-resources";
|
|
8
|
+
import { getLogger } from "@logtape/logtape";
|
|
9
|
+
|
|
10
|
+
import type { AnalysisResult } from "../../types.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
verboseLog,
|
|
14
|
+
verboseLogAnalysisResult,
|
|
15
|
+
verboseLogResourceStart,
|
|
16
|
+
} from "../utils.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Analyzes an Azure Private Endpoint for potential cost optimization.
|
|
20
|
+
*
|
|
21
|
+
* @param resource - The Azure resource object
|
|
22
|
+
* @param networkClient - Azure Network client for Private Endpoint details
|
|
23
|
+
* @returns Analysis result with cost risk and reason
|
|
24
|
+
*/
|
|
25
|
+
export async function analyzePrivateEndpoint(
|
|
26
|
+
resource: armResources.GenericResource,
|
|
27
|
+
networkClient: NetworkManagementClient,
|
|
28
|
+
verbose = false,
|
|
29
|
+
): Promise<AnalysisResult> {
|
|
30
|
+
verboseLogResourceStart(
|
|
31
|
+
verbose,
|
|
32
|
+
resource.name || "unknown",
|
|
33
|
+
"Private Endpoint (microsoft.network/privateendpoints)",
|
|
34
|
+
);
|
|
35
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
36
|
+
|
|
37
|
+
const costRisk: "high" | "low" | "medium" = "medium";
|
|
38
|
+
let reason = "";
|
|
39
|
+
|
|
40
|
+
if (!resource.id) {
|
|
41
|
+
return {
|
|
42
|
+
costRisk,
|
|
43
|
+
reason: "Resource ID is missing.",
|
|
44
|
+
suspectedUnused: false,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Extract resource group and private endpoint name from resource ID
|
|
49
|
+
const resourceParts = resource.id.split("/");
|
|
50
|
+
const resourceGroupName = resourceParts[4];
|
|
51
|
+
const privateEndpointName = resourceParts[8];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Get detailed Private Endpoint information
|
|
55
|
+
const privateEndpointDetails = await networkClient.privateEndpoints.get(
|
|
56
|
+
resourceGroupName,
|
|
57
|
+
privateEndpointName,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
verboseLog(
|
|
61
|
+
verbose,
|
|
62
|
+
"Private Endpoint API details:",
|
|
63
|
+
privateEndpointDetails,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Check if Private Endpoint has no private link service connection
|
|
67
|
+
if (
|
|
68
|
+
!privateEndpointDetails.privateLinkServiceConnections ||
|
|
69
|
+
privateEndpointDetails.privateLinkServiceConnections.length === 0
|
|
70
|
+
) {
|
|
71
|
+
reason +=
|
|
72
|
+
"Private Endpoint has no private link service connections configured. ";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check connection state
|
|
76
|
+
const hasFailedConnection =
|
|
77
|
+
privateEndpointDetails.privateLinkServiceConnections?.some(
|
|
78
|
+
(connection) =>
|
|
79
|
+
connection.privateLinkServiceConnectionState?.status === "Rejected" ||
|
|
80
|
+
connection.privateLinkServiceConnectionState?.status ===
|
|
81
|
+
"Disconnected",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (hasFailedConnection) {
|
|
85
|
+
reason += "Private Endpoint has rejected or disconnected connections. ";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check if Private Endpoint has network interfaces
|
|
89
|
+
if (
|
|
90
|
+
!privateEndpointDetails.networkInterfaces ||
|
|
91
|
+
privateEndpointDetails.networkInterfaces.length === 0
|
|
92
|
+
) {
|
|
93
|
+
reason += "Private Endpoint has no network interfaces attached. ";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check subnet configuration
|
|
97
|
+
if (!privateEndpointDetails.subnet) {
|
|
98
|
+
reason += "Private Endpoint is not associated with a subnet. ";
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
102
|
+
logger.warn(
|
|
103
|
+
`Failed to get Private Endpoint details for ${privateEndpointName}: ${error instanceof Error ? error.message : error}`,
|
|
104
|
+
);
|
|
105
|
+
reason += "Could not retrieve detailed Private Endpoint information. ";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const suspectedUnused = reason.length > 0;
|
|
109
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
110
|
+
verboseLogAnalysisResult(verbose, result);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure Public IP analysis
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
|
+
import type { NetworkManagementClient } from "@azure/arm-network";
|
|
7
|
+
|
|
8
|
+
import * as armResources from "@azure/arm-resources";
|
|
9
|
+
import { getLogger } from "@logtape/logtape";
|
|
10
|
+
|
|
11
|
+
import type { AnalysisResult } from "../../types.js";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getMetric,
|
|
15
|
+
verboseLog,
|
|
16
|
+
verboseLogAnalysisResult,
|
|
17
|
+
verboseLogResourceStart,
|
|
18
|
+
} from "../utils.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Analyzes an Azure Public IP for potential cost optimization.
|
|
22
|
+
*
|
|
23
|
+
* @param resource - The Azure resource object
|
|
24
|
+
* @param networkClient - Azure Network client for Public IP details
|
|
25
|
+
* @param monitorClient - Azure Monitor client for metrics
|
|
26
|
+
* @param timespanDays - Number of days to analyze metrics
|
|
27
|
+
* @returns Analysis result with cost risk and reason
|
|
28
|
+
*/
|
|
29
|
+
export async function analyzePublicIp(
|
|
30
|
+
resource: armResources.GenericResource,
|
|
31
|
+
networkClient: NetworkManagementClient,
|
|
32
|
+
monitorClient: MonitorClient,
|
|
33
|
+
timespanDays: number,
|
|
34
|
+
verbose = false,
|
|
35
|
+
): Promise<AnalysisResult> {
|
|
36
|
+
verboseLogResourceStart(
|
|
37
|
+
verbose,
|
|
38
|
+
resource.name || "unknown",
|
|
39
|
+
"Public IP (microsoft.network/publicipaddresses)",
|
|
40
|
+
);
|
|
41
|
+
verboseLog(verbose, "Resource details:", resource);
|
|
42
|
+
|
|
43
|
+
const costRisk: "high" | "low" | "medium" = "medium";
|
|
44
|
+
let reason = "";
|
|
45
|
+
|
|
46
|
+
if (!resource.id) {
|
|
47
|
+
return {
|
|
48
|
+
costRisk,
|
|
49
|
+
reason: "Resource ID is missing.",
|
|
50
|
+
suspectedUnused: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Extract resource group and public IP name from resource ID
|
|
55
|
+
const resourceParts = resource.id.split("/");
|
|
56
|
+
const resourceGroupName = resourceParts[4];
|
|
57
|
+
const publicIpName = resourceParts[8];
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Get detailed Public IP information
|
|
61
|
+
const publicIpDetails = await networkClient.publicIPAddresses.get(
|
|
62
|
+
resourceGroupName,
|
|
63
|
+
publicIpName,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
verboseLog(verbose, "Public IP API details:", publicIpDetails);
|
|
67
|
+
|
|
68
|
+
// Check if Public IP is not associated with any resource
|
|
69
|
+
if (!publicIpDetails.ipConfiguration && !publicIpDetails.natGateway) {
|
|
70
|
+
reason += "Public IP not associated with any resource. ";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if it's a static IP that might be unused
|
|
74
|
+
if (
|
|
75
|
+
publicIpDetails.publicIPAllocationMethod === "Static" &&
|
|
76
|
+
!publicIpDetails.ipConfiguration
|
|
77
|
+
) {
|
|
78
|
+
reason += "Static IP not in use. ";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check network metrics for low usage
|
|
82
|
+
const bytesInDDoS = await getMetric(
|
|
83
|
+
monitorClient,
|
|
84
|
+
resource.id,
|
|
85
|
+
"BytesInDDoS",
|
|
86
|
+
"Total",
|
|
87
|
+
timespanDays,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (bytesInDDoS !== null && bytesInDDoS < 1000000) {
|
|
91
|
+
// Less than 1MB total in 30 days
|
|
92
|
+
reason += `Very low network traffic (${(bytesInDDoS / 1024 / 1024).toFixed(2)} MB). `;
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
96
|
+
logger.warn(
|
|
97
|
+
`Failed to get Public IP details for ${publicIpName}: ${error instanceof Error ? error.message : error}`,
|
|
98
|
+
);
|
|
99
|
+
reason += "Could not retrieve detailed Public IP information. ";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const suspectedUnused = reason.length > 0;
|
|
103
|
+
const result = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
104
|
+
verboseLogAnalysisResult(verbose, result);
|
|
105
|
+
return result;
|
|
106
|
+
}
|