@pagopa/dx-savemoney 0.1.6 → 0.2.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 +104 -27
- package/dist/azure/analyzer.d.ts +4 -4
- package/dist/azure/analyzer.d.ts.map +1 -1
- package/dist/azure/analyzer.js +16 -10
- package/dist/azure/analyzer.js.map +1 -1
- package/dist/azure/config.d.ts +15 -3
- package/dist/azure/config.d.ts.map +1 -1
- package/dist/azure/config.js +35 -17
- package/dist/azure/config.js.map +1 -1
- package/dist/azure/report.d.ts +2 -2
- package/dist/azure/report.d.ts.map +1 -1
- package/dist/azure/report.js +68 -3
- package/dist/azure/report.js.map +1 -1
- package/dist/azure/resources/app-service.d.ts +2 -2
- package/dist/azure/resources/app-service.d.ts.map +1 -1
- package/dist/azure/resources/app-service.js +8 -5
- package/dist/azure/resources/app-service.js.map +1 -1
- package/dist/azure/resources/container-app.d.ts +2 -2
- package/dist/azure/resources/container-app.d.ts.map +1 -1
- package/dist/azure/resources/container-app.js +10 -8
- package/dist/azure/resources/container-app.js.map +1 -1
- package/dist/azure/resources/public-ip.d.ts +2 -2
- package/dist/azure/resources/public-ip.d.ts.map +1 -1
- package/dist/azure/resources/public-ip.js +3 -3
- package/dist/azure/resources/public-ip.js.map +1 -1
- package/dist/azure/resources/static-web-app.d.ts +2 -2
- package/dist/azure/resources/static-web-app.d.ts.map +1 -1
- package/dist/azure/resources/static-web-app.js +4 -5
- package/dist/azure/resources/static-web-app.js.map +1 -1
- package/dist/azure/resources/storage.d.ts +2 -2
- package/dist/azure/resources/storage.d.ts.map +1 -1
- package/dist/azure/resources/storage.js +4 -3
- package/dist/azure/resources/storage.js.map +1 -1
- package/dist/azure/resources/vm.d.ts +2 -2
- package/dist/azure/resources/vm.d.ts.map +1 -1
- package/dist/azure/resources/vm.js +4 -5
- package/dist/azure/resources/vm.js.map +1 -1
- package/dist/azure/types.d.ts +10 -2
- package/dist/azure/types.d.ts.map +1 -1
- package/dist/azure/utils.d.ts +9 -0
- package/dist/azure/utils.d.ts.map +1 -1
- package/dist/azure/utils.js +14 -0
- package/dist/azure/utils.js.map +1 -1
- package/dist/index.d.ts +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -54
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +210 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +97 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/package.json +10 -4
- package/src/azure/__tests__/config.test.ts +97 -0
- package/src/azure/__tests__/fixtures/full-override.yaml +25 -0
- package/src/azure/__tests__/fixtures/partial-override.yaml +10 -0
- package/src/azure/__tests__/report.test.ts +138 -0
- package/src/azure/__tests__/utils.test.ts +124 -0
- package/src/azure/analyzer.ts +24 -3
- package/src/azure/config.ts +36 -21
- package/src/azure/report.ts +81 -4
- package/src/azure/resources/__tests__/storage.test.ts +185 -0
- package/src/azure/resources/app-service.ts +13 -5
- package/src/azure/resources/container-app.ts +13 -4
- package/src/azure/resources/public-ip.ts +4 -3
- package/src/azure/resources/static-web-app.ts +5 -5
- package/src/azure/resources/storage.ts +7 -3
- package/src/azure/resources/vm.ts +5 -5
- package/src/azure/types.ts +15 -2
- package/src/azure/utils.ts +21 -0
- package/src/index.ts +19 -69
- package/src/schema.ts +134 -0
- package/src/types.ts +14 -0
package/README.md
CHANGED
|
@@ -80,7 +80,10 @@ yarn add @pagopa/dx-savemoney
|
|
|
80
80
|
- `table`: A human-readable summary for the terminal.
|
|
81
81
|
- `json`: Standard format for integration with other tools.
|
|
82
82
|
- `detailed-json`: A comprehensive output with all resource metadata, ideal for in-depth analysis via AI or custom scripts.
|
|
83
|
+
- `lint`: A linter-style output grouped by resource, with risk icons and a summary — ideal for CI pipelines and quick triage.
|
|
83
84
|
- **Simplified Configuration**: Supports configuration via files, command-line options, environment variables, or an interactive prompt.
|
|
85
|
+
- **Configurable Thresholds**: All analysis thresholds (CPU%, memory, transaction counts, etc.) can be overridden via the `thresholds` section of the YAML config file.
|
|
86
|
+
- **Tag Filtering**: Restrict the analysis to resources that match specific tag key-value pairs.
|
|
84
87
|
|
|
85
88
|
### Analyzed Resources
|
|
86
89
|
|
|
@@ -123,7 +126,7 @@ All resources are also checked for:
|
|
|
123
126
|
import { azure, loadConfig } from "@pagopa/dx-savemoney";
|
|
124
127
|
|
|
125
128
|
// Load configuration (from file, env vars, or interactive prompt)
|
|
126
|
-
const config = await loadConfig("./config.
|
|
129
|
+
const config = await loadConfig("./config.yaml");
|
|
127
130
|
|
|
128
131
|
// Run analysis and generate report
|
|
129
132
|
await azure.analyzeAzureResources(config, "table");
|
|
@@ -133,63 +136,139 @@ await azure.analyzeAzureResources(config, "table");
|
|
|
133
136
|
|
|
134
137
|
The tool requires the following configuration:
|
|
135
138
|
|
|
136
|
-
| Input | Type
|
|
137
|
-
| :------------------ |
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
139
|
+
| Input | Type | Required | Default | Description |
|
|
140
|
+
| :------------------ | :----------------------- | :------: | :----------- | :------------------------------------------------------------------------ |
|
|
141
|
+
| `subscriptionIds` | `string[]` | ✅ | - | Array of Azure subscription IDs to analyze |
|
|
142
|
+
| `preferredLocation` | `string` | ❌ | `italynorth` | Preferred Azure region (resources elsewhere will be flagged) |
|
|
143
|
+
| `timespanDays` | `number` | ❌ | `30` | Number of days to look back for metrics analysis |
|
|
144
|
+
| `verbose` | `boolean` | ❌ | `false` | Enable detailed logging for each resource analyzed |
|
|
145
|
+
| `filterTags` | `Record<string, string>` | ❌ | - | Only analyze resources matching **all** the specified tag key-value pairs |
|
|
146
|
+
| `thresholds` | `Thresholds` | ❌ | see below | Override the default numeric thresholds used during analysis |
|
|
143
147
|
|
|
144
148
|
#### Output Formats
|
|
145
149
|
|
|
146
150
|
The tool supports multiple output formats for different use cases:
|
|
147
151
|
|
|
148
|
-
| Format | Description
|
|
149
|
-
| :-------------- |
|
|
150
|
-
| `table` | Human-readable table in terminal
|
|
151
|
-
| `json` | Structured JSON with resource summaries
|
|
152
|
-
| `detailed-json` | Complete JSON with full Azure resource metadata
|
|
152
|
+
| Format | Description | Use Case |
|
|
153
|
+
| :-------------- | :-------------------------------------------------- | :----------------------------- |
|
|
154
|
+
| `table` | Human-readable table in terminal | Quick visual inspection |
|
|
155
|
+
| `json` | Structured JSON with resource summaries | Integration with other tools |
|
|
156
|
+
| `detailed-json` | Complete JSON with full Azure resource metadata | AI analysis or deep inspection |
|
|
157
|
+
| `lint` | Linter-style output grouped by resource, with icons | CI pipelines and quick triage |
|
|
153
158
|
|
|
154
159
|
#### How to Load Configuration
|
|
155
160
|
|
|
156
161
|
The `loadConfig()` function loads configuration in the following priority order:
|
|
157
162
|
|
|
158
163
|
1. **Configuration file** (pass file path as parameter)
|
|
159
|
-
2. **Environment
|
|
164
|
+
2. **Environment variable** (`ARM_SUBSCRIPTION_ID`)
|
|
160
165
|
3. **Interactive prompt** (if no other configuration is found)
|
|
161
166
|
|
|
162
167
|
**Example:**
|
|
163
168
|
|
|
164
169
|
```typescript
|
|
165
170
|
// From file
|
|
166
|
-
const config1 = await loadConfig("./config.
|
|
171
|
+
const config1 = await loadConfig("./config.yaml");
|
|
167
172
|
|
|
168
|
-
// From environment
|
|
173
|
+
// From environment variable or prompt
|
|
169
174
|
const config2 = await loadConfig();
|
|
170
175
|
```
|
|
171
176
|
|
|
172
177
|
#### Configuration File Example
|
|
173
178
|
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
```yaml
|
|
180
|
+
azure:
|
|
181
|
+
subscriptionIds:
|
|
182
|
+
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
183
|
+
preferredLocation: italynorth
|
|
184
|
+
timespanDays: 30
|
|
185
|
+
verbose: false
|
|
186
|
+
thresholds: # optional — omit to use built-in defaults
|
|
187
|
+
vm:
|
|
188
|
+
cpuPercent: 5
|
|
189
|
+
storage:
|
|
190
|
+
transactionsPerDay: 50
|
|
182
191
|
```
|
|
183
192
|
|
|
193
|
+
#### Thresholds Configuration
|
|
194
|
+
|
|
195
|
+
All numeric thresholds used during analysis can be overridden by adding a
|
|
196
|
+
`thresholds` section inside the `azure` block of your config YAML.
|
|
197
|
+
Only the fields you want to change are required — all others keep their
|
|
198
|
+
built-in defaults.
|
|
199
|
+
|
|
200
|
+
**Example (full override, `config.yaml`):**
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
azure:
|
|
204
|
+
subscriptionIds:
|
|
205
|
+
- xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
206
|
+
thresholds:
|
|
207
|
+
vm:
|
|
208
|
+
cpuPercent: 5
|
|
209
|
+
networkInBytesPerDay: 10485760
|
|
210
|
+
appService:
|
|
211
|
+
cpuPercent: 10
|
|
212
|
+
memoryPercent: 20
|
|
213
|
+
premiumCpuPercent: 15
|
|
214
|
+
containerApp:
|
|
215
|
+
cpuNanoCores: 5000000
|
|
216
|
+
memoryBytes: 52428800
|
|
217
|
+
networkBytes: 100000
|
|
218
|
+
storage:
|
|
219
|
+
transactionsPerDay: 50
|
|
220
|
+
publicIp:
|
|
221
|
+
bytesInDDoS: 1048576
|
|
222
|
+
staticSite:
|
|
223
|
+
siteHits: 500
|
|
224
|
+
bytesSent: 5242880
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Default threshold values:**
|
|
228
|
+
|
|
229
|
+
| Resource | Field | Default Value | Description |
|
|
230
|
+
| :------------- | :--------------------- | :---------------------- | :-------------------------------------------- |
|
|
231
|
+
| `vm` | `cpuPercent` | `1` (%) | Average CPU below which VM is flagged |
|
|
232
|
+
| `vm` | `networkInBytesPerDay` | `3145728` (3 MB) | Average inbound traffic below which flagged |
|
|
233
|
+
| `appService` | `cpuPercent` | `5` (%) | Average CPU below which plan is flagged |
|
|
234
|
+
| `appService` | `memoryPercent` | `10` (%) | Average memory below which plan is flagged |
|
|
235
|
+
| `appService` | `premiumCpuPercent` | `10` (%) | CPU threshold for Premium-tier over-provision |
|
|
236
|
+
| `containerApp` | `cpuNanoCores` | `1000000` (0.001 cores) | Average CPU below which app is flagged |
|
|
237
|
+
| `containerApp` | `memoryBytes` | `10485760` (10 MB) | Average memory below which app is flagged |
|
|
238
|
+
| `containerApp` | `networkBytes` | `34000` (~33 KB) | Combined Rx+Tx below which app is flagged |
|
|
239
|
+
| `storage` | `transactionsPerDay` | `10` | Avg daily transactions below which flagged |
|
|
240
|
+
| `publicIp` | `bytesInDDoS` | `340000` (~332 KB) | Avg inbound bytes/day below which flagged |
|
|
241
|
+
| `staticSite` | `siteHits` | `100` | Total requests below which site is flagged |
|
|
242
|
+
| `staticSite` | `bytesSent` | `1048576` (1 MB) | Total bytes sent below which site is flagged |
|
|
243
|
+
|
|
184
244
|
#### Usage Examples
|
|
185
245
|
|
|
246
|
+
##### Tag Filtering
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { azure, loadConfig } from "@pagopa/dx-savemoney";
|
|
250
|
+
|
|
251
|
+
const config = await loadConfig("./config.yaml");
|
|
252
|
+
// Analyze only resources tagged with environment=prod AND team=platform
|
|
253
|
+
await azure.analyzeAzureResources(
|
|
254
|
+
{
|
|
255
|
+
...config,
|
|
256
|
+
filterTags: new Map([
|
|
257
|
+
["environment", "prod"],
|
|
258
|
+
["team", "platform"],
|
|
259
|
+
]),
|
|
260
|
+
},
|
|
261
|
+
"lint",
|
|
262
|
+
);
|
|
263
|
+
```
|
|
264
|
+
|
|
186
265
|
##### Basic Usage
|
|
187
266
|
|
|
188
267
|
```typescript
|
|
189
268
|
import { azure, loadConfig } from "@pagopa/dx-savemoney";
|
|
190
269
|
|
|
191
270
|
// Load from config file
|
|
192
|
-
const config = await loadConfig("./config.
|
|
271
|
+
const config = await loadConfig("./config.yaml");
|
|
193
272
|
await azure.analyzeAzureResources(config, "table");
|
|
194
273
|
```
|
|
195
274
|
|
|
@@ -200,7 +279,6 @@ import { azure } from "@pagopa/dx-savemoney";
|
|
|
200
279
|
import type { AzureConfig } from "@pagopa/dx-savemoney";
|
|
201
280
|
|
|
202
281
|
const config: AzureConfig = {
|
|
203
|
-
tenantId: "your-tenant-id",
|
|
204
282
|
subscriptionIds: ["sub-id-1", "sub-id-2"],
|
|
205
283
|
preferredLocation: "italynorth",
|
|
206
284
|
timespanDays: 30,
|
|
@@ -226,7 +304,6 @@ await azure.analyzeAzureResources(config, "detailed-json");
|
|
|
226
304
|
import { loadConfig, azure } from "@pagopa/dx-savemoney";
|
|
227
305
|
|
|
228
306
|
// Set environment variables
|
|
229
|
-
// ARM_TENANT_ID=xxx
|
|
230
307
|
// ARM_SUBSCRIPTION_ID=sub1,sub2
|
|
231
308
|
|
|
232
309
|
const config = await loadConfig(); // Will read from env vars
|
package/dist/azure/analyzer.d.ts
CHANGED
|
@@ -8,14 +8,14 @@ import { MonitorClient } from "@azure/arm-monitor";
|
|
|
8
8
|
import { NetworkManagementClient } from "@azure/arm-network";
|
|
9
9
|
import * as armResources from "@azure/arm-resources";
|
|
10
10
|
import type { AzureConfig } from "./types.js";
|
|
11
|
-
import { type AnalysisResult } from "../types.js";
|
|
11
|
+
import { type AnalysisResult, type Thresholds } from "../types.js";
|
|
12
12
|
/**
|
|
13
13
|
* Analyzes resources in multiple Azure subscriptions and generates a report.
|
|
14
14
|
*
|
|
15
15
|
* @param config - Azure configuration with subscription IDs and settings
|
|
16
|
-
* @param format - Output format (table, json,
|
|
16
|
+
* @param format - Output format (table, json, detailed-json, or lint)
|
|
17
17
|
*/
|
|
18
|
-
export declare function analyzeAzureResources(config: AzureConfig, format: "detailed-json" | "json" | "table"): Promise<void>;
|
|
18
|
+
export declare function analyzeAzureResources(config: AzureConfig, format: "detailed-json" | "json" | "lint" | "table"): Promise<void>;
|
|
19
19
|
/**
|
|
20
20
|
* Analyzes a single Azure resource based on its type.
|
|
21
21
|
*
|
|
@@ -30,5 +30,5 @@ export declare function analyzeAzureResources(config: AzureConfig, format: "deta
|
|
|
30
30
|
* @param verbose - Whether verbose logging is enabled
|
|
31
31
|
* @returns Analysis result with cost risk and reason
|
|
32
32
|
*/
|
|
33
|
-
export declare function analyzeResource(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, networkClient: NetworkManagementClient, webSiteClient: WebSiteManagementClient, containerAppsClient: ContainerAppsAPIClient, preferredLocation: string, timespanDays: number, verbose?: boolean): Promise<AnalysisResult>;
|
|
33
|
+
export declare function analyzeResource(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, networkClient: NetworkManagementClient, webSiteClient: WebSiteManagementClient, containerAppsClient: ContainerAppsAPIClient, preferredLocation: string, timespanDays: number, thresholds: Thresholds, verbose?: boolean): Promise<AnalysisResult>;
|
|
34
34
|
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,WAAW,EAA+B,MAAM,YAAY,CAAC;AAE3E,OAAO,
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAIrD,OAAO,KAAK,EAAE,WAAW,EAA+B,MAAM,YAAY,CAAC;AAE3E,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,UAAU,EAChB,MAAM,aAAa,CAAC;AAerB;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,iBA2EpD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,uBAAuB,EACtC,aAAa,EAAE,uBAAuB,EACtC,aAAa,EAAE,uBAAuB,EACtC,mBAAmB,EAAE,sBAAsB,EAC3C,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,UAAU,EACtB,OAAO,UAAQ,GACd,OAAO,CAAC,cAAc,CAAC,CAuHzB"}
|
package/dist/azure/analyzer.js
CHANGED
|
@@ -9,14 +9,15 @@ import { NetworkManagementClient } from "@azure/arm-network";
|
|
|
9
9
|
import * as armResources from "@azure/arm-resources";
|
|
10
10
|
import { DefaultAzureCredential } from "@azure/identity";
|
|
11
11
|
import { getLogger } from "@logtape/logtape";
|
|
12
|
-
import { mergeResults } from "../types.js";
|
|
12
|
+
import { DEFAULT_THRESHOLDS, mergeResults, } from "../types.js";
|
|
13
13
|
import { generateReport } from "./report.js";
|
|
14
14
|
import { analyzeAppServicePlan, analyzeContainerApp, analyzeDisk, analyzeNic, analyzePrivateEndpoint, analyzePublicIp, analyzeStaticSite, analyzeStorageAccount, analyzeVM, } from "./resources/index.js";
|
|
15
|
+
import { matchesTags } from "./utils.js";
|
|
15
16
|
/**
|
|
16
17
|
* Analyzes resources in multiple Azure subscriptions and generates a report.
|
|
17
18
|
*
|
|
18
19
|
* @param config - Azure configuration with subscription IDs and settings
|
|
19
|
-
* @param format - Output format (table, json,
|
|
20
|
+
* @param format - Output format (table, json, detailed-json, or lint)
|
|
20
21
|
*/
|
|
21
22
|
export async function analyzeAzureResources(config, format) {
|
|
22
23
|
const logger = getLogger(["savemoney", "azure"]);
|
|
@@ -30,9 +31,14 @@ export async function analyzeAzureResources(config, format) {
|
|
|
30
31
|
const networkClient = new NetworkManagementClient(credential, subscriptionId.trim());
|
|
31
32
|
const webSiteClient = new WebSiteManagementClient(credential, subscriptionId.trim());
|
|
32
33
|
const containerAppsClient = new ContainerAppsAPIClient(credential, subscriptionId.trim());
|
|
34
|
+
const thresholds = config.thresholds ?? DEFAULT_THRESHOLDS;
|
|
33
35
|
// Use the async iterator to avoid memory explosion for large environments
|
|
34
36
|
for await (const resource of resourceClient.resources.list()) {
|
|
35
|
-
|
|
37
|
+
// Skip resources that don't match the requested tag filter
|
|
38
|
+
if (!matchesTags(resource, config.filterTags)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const { costRisk, reason, suspectedUnused } = await analyzeResource(resource, monitorClient, computeClient, networkClient, webSiteClient, containerAppsClient, config.preferredLocation, config.timespanDays, thresholds, config.verbose || false);
|
|
36
42
|
if (suspectedUnused) {
|
|
37
43
|
allReports.push({
|
|
38
44
|
analysis: {
|
|
@@ -68,7 +74,7 @@ export async function analyzeAzureResources(config, format) {
|
|
|
68
74
|
* @param verbose - Whether verbose logging is enabled
|
|
69
75
|
* @returns Analysis result with cost risk and reason
|
|
70
76
|
*/
|
|
71
|
-
export async function analyzeResource(resource, monitorClient, computeClient, networkClient, webSiteClient, containerAppsClient, preferredLocation, timespanDays, verbose = false) {
|
|
77
|
+
export async function analyzeResource(resource, monitorClient, computeClient, networkClient, webSiteClient, containerAppsClient, preferredLocation, timespanDays, thresholds, verbose = false) {
|
|
72
78
|
const type = resource.type?.toLowerCase() || "";
|
|
73
79
|
let result = {
|
|
74
80
|
costRisk: "low",
|
|
@@ -83,7 +89,7 @@ export async function analyzeResource(resource, monitorClient, computeClient, ne
|
|
|
83
89
|
// Route to type-specific analysis hooks
|
|
84
90
|
switch (type) {
|
|
85
91
|
case "microsoft.app/containerapps": {
|
|
86
|
-
const containerAppResult = await analyzeContainerApp(resource, containerAppsClient, monitorClient, timespanDays, verbose);
|
|
92
|
+
const containerAppResult = await analyzeContainerApp(resource, containerAppsClient, monitorClient, timespanDays, thresholds, verbose);
|
|
87
93
|
result = mergeResults(result, containerAppResult);
|
|
88
94
|
break;
|
|
89
95
|
}
|
|
@@ -93,7 +99,7 @@ export async function analyzeResource(resource, monitorClient, computeClient, ne
|
|
|
93
99
|
break;
|
|
94
100
|
}
|
|
95
101
|
case "microsoft.compute/virtualmachines": {
|
|
96
|
-
const vmResult = await analyzeVM(resource, monitorClient, computeClient, timespanDays, verbose);
|
|
102
|
+
const vmResult = await analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds, verbose);
|
|
97
103
|
result = mergeResults(result, vmResult);
|
|
98
104
|
break;
|
|
99
105
|
}
|
|
@@ -108,22 +114,22 @@ export async function analyzeResource(resource, monitorClient, computeClient, ne
|
|
|
108
114
|
break;
|
|
109
115
|
}
|
|
110
116
|
case "microsoft.network/publicipaddresses": {
|
|
111
|
-
const pipResult = await analyzePublicIp(resource, networkClient, monitorClient, timespanDays, verbose);
|
|
117
|
+
const pipResult = await analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds, verbose);
|
|
112
118
|
result = mergeResults(result, pipResult);
|
|
113
119
|
break;
|
|
114
120
|
}
|
|
115
121
|
case "microsoft.storage/storageaccounts": {
|
|
116
|
-
const storageResult = await analyzeStorageAccount(resource, monitorClient, timespanDays, verbose);
|
|
122
|
+
const storageResult = await analyzeStorageAccount(resource, monitorClient, timespanDays, thresholds, verbose);
|
|
117
123
|
result = mergeResults(result, storageResult);
|
|
118
124
|
break;
|
|
119
125
|
}
|
|
120
126
|
case "microsoft.web/serverfarms": {
|
|
121
|
-
const aspResult = await analyzeAppServicePlan(resource, webSiteClient, monitorClient, timespanDays, verbose);
|
|
127
|
+
const aspResult = await analyzeAppServicePlan(resource, webSiteClient, monitorClient, timespanDays, thresholds, verbose);
|
|
122
128
|
result = mergeResults(result, aspResult);
|
|
123
129
|
break;
|
|
124
130
|
}
|
|
125
131
|
case "microsoft.web/staticsites": {
|
|
126
|
-
const staticSiteResult = await analyzeStaticSite(resource, monitorClient, timespanDays, verbose);
|
|
132
|
+
const staticSiteResult = await analyzeStaticSite(resource, monitorClient, timespanDays, thresholds, verbose);
|
|
127
133
|
result = mergeResults(result, staticSiteResult);
|
|
128
134
|
break;
|
|
129
135
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI7C,OAAO,
|
|
1
|
+
{"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"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"}
|
package/dist/azure/config.d.ts
CHANGED
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { AzureConfig } from "./types.js";
|
|
5
5
|
/**
|
|
6
|
-
* Loads Azure configuration from file, environment variables, or interactive prompts.
|
|
6
|
+
* Loads Azure configuration from a YAML file, environment variables, or interactive prompts.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The YAML file must have an `azure` top-level key:
|
|
9
|
+
* ```yaml
|
|
10
|
+
* azure:
|
|
11
|
+
* subscriptionIds:
|
|
12
|
+
* - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
13
|
+
* preferredLocation: italynorth
|
|
14
|
+
* timespanDays: 30
|
|
15
|
+
* thresholds:
|
|
16
|
+
* vm:
|
|
17
|
+
* cpuPercent: 5
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @param configPath - Optional path to a YAML configuration file
|
|
21
|
+
* @returns Azure configuration object with subscription IDs, settings and thresholds
|
|
10
22
|
*/
|
|
11
23
|
export declare function loadAzureConfig(configPath?: string): Promise<AzureConfig>;
|
|
12
24
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/azure/config.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/azure/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI9C;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,eAAe,CACnC,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC,CA0CtB;AAED;;;;;GAKG;AACH,wBAAsB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9D"}
|
package/dist/azure/config.js
CHANGED
|
@@ -3,43 +3,61 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { getLogger } from "@logtape/logtape";
|
|
5
5
|
import * as fs from "fs";
|
|
6
|
+
import * as yaml from "js-yaml";
|
|
6
7
|
import * as readline from "readline";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { ConfigSchema } from "../schema.js";
|
|
7
10
|
/**
|
|
8
|
-
* Loads Azure configuration from file, environment variables, or interactive prompts.
|
|
11
|
+
* Loads Azure configuration from a YAML file, environment variables, or interactive prompts.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
13
|
+
* The YAML file must have an `azure` top-level key:
|
|
14
|
+
* ```yaml
|
|
15
|
+
* azure:
|
|
16
|
+
* subscriptionIds:
|
|
17
|
+
* - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
18
|
+
* preferredLocation: italynorth
|
|
19
|
+
* timespanDays: 30
|
|
20
|
+
* thresholds:
|
|
21
|
+
* vm:
|
|
22
|
+
* cpuPercent: 5
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param configPath - Optional path to a YAML configuration file
|
|
26
|
+
* @returns Azure configuration object with subscription IDs, settings and thresholds
|
|
12
27
|
*/
|
|
13
28
|
export async function loadAzureConfig(configPath) {
|
|
14
|
-
if (configPath
|
|
29
|
+
if (configPath) {
|
|
30
|
+
if (!fs.existsSync(configPath)) {
|
|
31
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
32
|
+
}
|
|
15
33
|
try {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
throw new Error("Config file must contain 'tenantId' and 'subscriptionIds'");
|
|
20
|
-
}
|
|
34
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
35
|
+
const rawYaml = yaml.load(raw);
|
|
36
|
+
const parsed = ConfigSchema.parse(rawYaml);
|
|
21
37
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
timespanDays: config.timespanDays || 30,
|
|
38
|
+
preferredLocation: parsed.azure.preferredLocation,
|
|
39
|
+
subscriptionIds: parsed.azure.subscriptionIds,
|
|
40
|
+
thresholds: parsed.azure.thresholds,
|
|
41
|
+
timespanDays: parsed.azure.timespanDays,
|
|
27
42
|
};
|
|
28
43
|
}
|
|
29
44
|
catch (error) {
|
|
30
|
-
|
|
45
|
+
if (error instanceof z.ZodError) {
|
|
46
|
+
throw new Error(`Invalid config file:\n${z.prettifyError(error)}`, {
|
|
47
|
+
cause: error,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`Failed to load config file: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
31
51
|
}
|
|
32
52
|
}
|
|
33
53
|
const logger = getLogger(["savemoney", "azure", "config"]);
|
|
34
54
|
logger.info("Configuration file not found. Checking environment variables...");
|
|
35
|
-
const tenantId = process.env.ARM_TENANT_ID || (await prompt("Enter Tenant ID: "));
|
|
36
55
|
const subscriptionIds = process.env.ARM_SUBSCRIPTION_ID
|
|
37
56
|
? process.env.ARM_SUBSCRIPTION_ID.split(",")
|
|
38
57
|
: (await prompt("Enter Subscription IDs (comma-separated): ")).split(",");
|
|
39
58
|
return {
|
|
40
59
|
preferredLocation: "italynorth",
|
|
41
60
|
subscriptionIds,
|
|
42
|
-
tenantId,
|
|
43
61
|
timespanDays: 30,
|
|
44
62
|
};
|
|
45
63
|
}
|
package/dist/azure/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/azure/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/azure/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,SAAS,CAAC;AAChC,OAAO,KAAK,QAAQ,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAmB;IAEnB,IAAI,UAAU,EAAE,CAAC;QACf,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CAAC,0BAA0B,UAAU,EAAE,CAAC,CAAC;QAC1D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC/B,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC3C,OAAO;gBACL,iBAAiB,EAAE,MAAM,CAAC,KAAK,CAAC,iBAAiB;gBACjD,eAAe,EAAE,MAAM,CAAC,KAAK,CAAC,eAAe;gBAC7C,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU;gBACnC,YAAY,EAAE,MAAM,CAAC,KAAK,CAAC,YAAY;aACxC,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAChC,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE;oBACjE,KAAK,EAAE,KAAK;iBACb,CAAC,CAAC;YACL,CAAC;YACD,MAAM,IAAI,KAAK,CACb,+BAA+B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,EAC/E,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,WAAW,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC3D,MAAM,CAAC,IAAI,CACT,iEAAiE,CAClE,CAAC;IAEF,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,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/azure/report.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { AzureDetailedResourceReport } from "./types.js";
|
|
|
6
6
|
* Generates a report from Azure resource analysis in the specified format.
|
|
7
7
|
*
|
|
8
8
|
* @param report - Array of detailed resource reports
|
|
9
|
-
* @param format - Output format (table, json,
|
|
9
|
+
* @param format - Output format (table, json, detailed-json, or lint)
|
|
10
10
|
*/
|
|
11
|
-
export declare function generateReport(report: AzureDetailedResourceReport[], format: "detailed-json" | "json" | "table"): Promise<void>;
|
|
11
|
+
export declare function generateReport(report: AzureDetailedResourceReport[], format: "detailed-json" | "json" | "lint" | "table"): Promise<void>;
|
|
12
12
|
//# sourceMappingURL=report.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/azure/report.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,2BAA2B,EAE5B,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/azure/report.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,2BAA2B,EAE5B,MAAM,YAAY,CAAC;AAuBpB;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,2BAA2B,EAAE,EACrC,MAAM,EAAE,eAAe,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,iBAmCpD"}
|
package/dist/azure/report.js
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Azure report generation
|
|
3
3
|
*/
|
|
4
|
+
// ANSI color codes — only applied when stdout is a TTY to avoid cluttering redirected output
|
|
5
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
6
|
+
const RED = isTTY ? "\x1b[31m" : "";
|
|
7
|
+
const YELLOW = isTTY ? "\x1b[33m" : "";
|
|
8
|
+
const BLUE = isTTY ? "\x1b[34m" : "";
|
|
9
|
+
const BOLD = isTTY ? "\x1b[1m" : "";
|
|
10
|
+
const RESET = isTTY ? "\x1b[0m" : "";
|
|
11
|
+
const DIM = isTTY ? "\x1b[2m" : "";
|
|
12
|
+
const RISK_ICON = {
|
|
13
|
+
high: `${RED}✖${RESET}`,
|
|
14
|
+
low: `${BLUE}ℹ${RESET}`,
|
|
15
|
+
medium: `${YELLOW}⚠${RESET}`,
|
|
16
|
+
};
|
|
17
|
+
const RISK_COLOR = {
|
|
18
|
+
high: RED,
|
|
19
|
+
low: BLUE,
|
|
20
|
+
medium: YELLOW,
|
|
21
|
+
};
|
|
4
22
|
/**
|
|
5
23
|
* Generates a report from Azure resource analysis in the specified format.
|
|
6
24
|
*
|
|
7
25
|
* @param report - Array of detailed resource reports
|
|
8
|
-
* @param format - Output format (table, json,
|
|
26
|
+
* @param format - Output format (table, json, detailed-json, or lint)
|
|
9
27
|
*/
|
|
10
28
|
export async function generateReport(report, format) {
|
|
11
29
|
if (format === "detailed-json") {
|
|
@@ -26,6 +44,9 @@ export async function generateReport(report, format) {
|
|
|
26
44
|
if (format === "json") {
|
|
27
45
|
console.log(JSON.stringify(summaryReport, null, 2));
|
|
28
46
|
}
|
|
47
|
+
else if (format === "lint") {
|
|
48
|
+
generateLintReport(report);
|
|
49
|
+
}
|
|
29
50
|
else {
|
|
30
51
|
console.table(summaryReport.map((r) => ({
|
|
31
52
|
Name: r.name,
|
|
@@ -33,8 +54,52 @@ export async function generateReport(report, format) {
|
|
|
33
54
|
"Resource Group": r.resourceGroup || "N/A",
|
|
34
55
|
Risk: r.costRisk,
|
|
35
56
|
Type: r.type,
|
|
36
|
-
|
|
37
|
-
})), ["Name", "Type", "Resource Group", "Risk", "Unused", "Reason"]);
|
|
57
|
+
})), ["Name", "Type", "Resource Group", "Risk", "Reason"]);
|
|
38
58
|
}
|
|
39
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Renders a linter-style report to stdout, grouping findings by resource.
|
|
62
|
+
*
|
|
63
|
+
* Example output:
|
|
64
|
+
*
|
|
65
|
+
* /subscriptions/.../virtualMachines/my-vm
|
|
66
|
+
* ✖ HIGH VM is deallocated.
|
|
67
|
+
* ✖ HIGH No tags found.
|
|
68
|
+
*
|
|
69
|
+
* Summary: 3 issues found (2 high, 0 medium, 1 low)
|
|
70
|
+
*/
|
|
71
|
+
function generateLintReport(report) {
|
|
72
|
+
const counts = { high: 0, low: 0, medium: 0 };
|
|
73
|
+
for (const entry of report) {
|
|
74
|
+
const resourceId = entry.resource.id ?? `unknown/${entry.resource.name ?? "unknown"}`;
|
|
75
|
+
const risk = entry.analysis.costRisk;
|
|
76
|
+
const findings = splitReasons(entry.analysis.reason);
|
|
77
|
+
console.log(`${BOLD}${resourceId}${RESET}`);
|
|
78
|
+
for (const finding of findings) {
|
|
79
|
+
const icon = RISK_ICON[risk];
|
|
80
|
+
const color = RISK_COLOR[risk];
|
|
81
|
+
const label = `${color}${risk.toUpperCase().padEnd(6)}${RESET}`;
|
|
82
|
+
console.log(` ${icon} ${label} ${DIM}${finding}${RESET}`);
|
|
83
|
+
counts[risk]++;
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
}
|
|
87
|
+
const total = counts.high + counts.medium + counts.low;
|
|
88
|
+
const summaryLine = `${BOLD}Summary:${RESET} ${total} issue${total !== 1 ? "s" : ""} found` +
|
|
89
|
+
` ${RED}(${counts.high} high${RESET}` +
|
|
90
|
+
`, ${YELLOW}${counts.medium} medium${RESET}` +
|
|
91
|
+
`, ${BLUE}${counts.low} low${RESET})`;
|
|
92
|
+
console.log(summaryLine);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Splits a concatenated reason string (sentences separated by ". ") into
|
|
96
|
+
* individual finding strings, stripping trailing whitespace.
|
|
97
|
+
*/
|
|
98
|
+
function splitReasons(reason) {
|
|
99
|
+
return reason
|
|
100
|
+
.split(/\.\s+/)
|
|
101
|
+
.map((s) => s.trim())
|
|
102
|
+
.filter((s) => s.length > 0)
|
|
103
|
+
.map((s) => (s.endsWith(".") ? s : `${s}.`));
|
|
104
|
+
}
|
|
40
105
|
//# sourceMappingURL=report.js.map
|
package/dist/azure/report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/azure/report.ts"],"names":[],"mappings":"AAAA;;GAEG;AAOH;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,MAAqC,EACrC,
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../../src/azure/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import type { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
5
5
|
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
6
|
import * as armResources from "@azure/arm-resources";
|
|
7
|
-
import type { AnalysisResult } from "../../types.js";
|
|
7
|
+
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
8
8
|
/**
|
|
9
9
|
* Analyzes an Azure App Service Plan for potential cost optimization.
|
|
10
10
|
*
|
|
@@ -14,5 +14,5 @@ import type { AnalysisResult } from "../../types.js";
|
|
|
14
14
|
* @param timespanDays - Number of days to analyze metrics
|
|
15
15
|
* @returns Analysis result with cost risk and reason
|
|
16
16
|
*/
|
|
17
|
-
export declare function analyzeAppServicePlan(resource: armResources.GenericResource, webSiteClient: WebSiteManagementClient, monitorClient: MonitorClient, timespanDays: number, verbose?: boolean): Promise<AnalysisResult>;
|
|
17
|
+
export declare function analyzeAppServicePlan(resource: armResources.GenericResource, webSiteClient: WebSiteManagementClient, monitorClient: MonitorClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean): Promise<AnalysisResult>;
|
|
18
18
|
//# sourceMappingURL=app-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-service.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"app-service.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/app-service.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAUjE;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,uBAAuB,EACtC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,UAA+B,EAC3C,OAAO,UAAQ,GACd,OAAO,CAAC,cAAc,CAAC,CAyFzB"}
|