@pagopa/dx-savemoney 0.2.5 → 0.3.0

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 (103) hide show
  1. package/README.md +33 -27
  2. package/dist/azure/analyzer.d.ts +49 -21
  3. package/dist/azure/analyzer.d.ts.map +1 -1
  4. package/dist/azure/analyzer.js +369 -93
  5. package/dist/azure/analyzer.js.map +1 -1
  6. package/dist/azure/analyzers/advisor.d.ts +68 -0
  7. package/dist/azure/analyzers/advisor.d.ts.map +1 -0
  8. package/dist/azure/analyzers/advisor.js +234 -0
  9. package/dist/azure/analyzers/advisor.js.map +1 -0
  10. package/dist/azure/analyzers/index.d.ts +8 -0
  11. package/dist/azure/analyzers/index.d.ts.map +1 -0
  12. package/dist/azure/analyzers/index.js +6 -0
  13. package/dist/azure/analyzers/index.js.map +1 -0
  14. package/dist/azure/analyzers/registry.d.ts +29 -0
  15. package/dist/azure/analyzers/registry.d.ts.map +1 -0
  16. package/dist/azure/analyzers/registry.js +79 -0
  17. package/dist/azure/analyzers/registry.js.map +1 -0
  18. package/dist/azure/analyzers/subscription.d.ts +53 -0
  19. package/dist/azure/analyzers/subscription.d.ts.map +1 -0
  20. package/dist/azure/analyzers/subscription.js +18 -0
  21. package/dist/azure/analyzers/subscription.js.map +1 -0
  22. package/dist/azure/analyzers/types.d.ts +62 -0
  23. package/dist/azure/analyzers/types.d.ts.map +1 -0
  24. package/dist/azure/analyzers/types.js +15 -0
  25. package/dist/azure/analyzers/types.js.map +1 -0
  26. package/dist/azure/config.d.ts.map +1 -1
  27. package/dist/azure/config.js +2 -0
  28. package/dist/azure/config.js.map +1 -1
  29. package/dist/azure/index.d.ts +1 -0
  30. package/dist/azure/index.d.ts.map +1 -1
  31. package/dist/azure/index.js +1 -0
  32. package/dist/azure/index.js.map +1 -1
  33. package/dist/azure/report.d.ts.map +1 -1
  34. package/dist/azure/report.js +178 -29
  35. package/dist/azure/report.js.map +1 -1
  36. package/dist/azure/resources/app-service.d.ts +2 -1
  37. package/dist/azure/resources/app-service.d.ts.map +1 -1
  38. package/dist/azure/resources/app-service.js +3 -3
  39. package/dist/azure/resources/app-service.js.map +1 -1
  40. package/dist/azure/resources/container-app.d.ts +2 -1
  41. package/dist/azure/resources/container-app.d.ts.map +1 -1
  42. package/dist/azure/resources/container-app.js +9 -9
  43. package/dist/azure/resources/container-app.js.map +1 -1
  44. package/dist/azure/resources/public-ip.d.ts +2 -1
  45. package/dist/azure/resources/public-ip.d.ts.map +1 -1
  46. package/dist/azure/resources/public-ip.js +2 -2
  47. package/dist/azure/resources/public-ip.js.map +1 -1
  48. package/dist/azure/resources/static-web-app.d.ts +2 -1
  49. package/dist/azure/resources/static-web-app.d.ts.map +1 -1
  50. package/dist/azure/resources/static-web-app.js +3 -3
  51. package/dist/azure/resources/static-web-app.js.map +1 -1
  52. package/dist/azure/resources/storage.d.ts +2 -1
  53. package/dist/azure/resources/storage.d.ts.map +1 -1
  54. package/dist/azure/resources/storage.js +2 -2
  55. package/dist/azure/resources/storage.js.map +1 -1
  56. package/dist/azure/resources/vm.d.ts +2 -1
  57. package/dist/azure/resources/vm.d.ts.map +1 -1
  58. package/dist/azure/resources/vm.js +3 -3
  59. package/dist/azure/resources/vm.js.map +1 -1
  60. package/dist/azure/types.d.ts +34 -1
  61. package/dist/azure/types.d.ts.map +1 -1
  62. package/dist/azure/utils.d.ts +35 -3
  63. package/dist/azure/utils.d.ts.map +1 -1
  64. package/dist/azure/utils.js +70 -29
  65. package/dist/azure/utils.js.map +1 -1
  66. package/dist/finding.d.ts +114 -0
  67. package/dist/finding.d.ts.map +1 -0
  68. package/dist/finding.js +51 -0
  69. package/dist/finding.js.map +1 -0
  70. package/dist/index.d.ts +4 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +3 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/schema.d.ts +5 -0
  75. package/dist/schema.d.ts.map +1 -1
  76. package/dist/schema.js +14 -0
  77. package/dist/schema.js.map +1 -1
  78. package/package.json +4 -1
  79. package/src/__tests__/finding.test.ts +149 -0
  80. package/src/azure/__tests__/analyzer-tags.test.ts +74 -0
  81. package/src/azure/__tests__/report.test.ts +27 -0
  82. package/src/azure/__tests__/utils.test.ts +164 -2
  83. package/src/azure/analyzer.ts +513 -182
  84. package/src/azure/analyzers/__tests__/advisor.test.ts +367 -0
  85. package/src/azure/analyzers/advisor.ts +324 -0
  86. package/src/azure/analyzers/index.ts +14 -0
  87. package/src/azure/analyzers/registry.ts +196 -0
  88. package/src/azure/analyzers/subscription.ts +56 -0
  89. package/src/azure/analyzers/types.ts +66 -0
  90. package/src/azure/config.ts +2 -0
  91. package/src/azure/index.ts +1 -0
  92. package/src/azure/report.ts +206 -35
  93. package/src/azure/resources/app-service.ts +4 -0
  94. package/src/azure/resources/container-app.ts +10 -0
  95. package/src/azure/resources/public-ip.ts +3 -0
  96. package/src/azure/resources/static-web-app.ts +4 -0
  97. package/src/azure/resources/storage.ts +3 -0
  98. package/src/azure/resources/vm.ts +4 -0
  99. package/src/azure/types.ts +35 -1
  100. package/src/azure/utils.ts +110 -39
  101. package/src/finding.ts +152 -0
  102. package/src/index.ts +19 -1
  103. package/src/schema.ts +14 -0
@@ -1,5 +1,22 @@
1
1
  /**
2
2
  * Azure resource analyzer - Main orchestration logic
3
+ *
4
+ * Iterates every resource in every configured subscription, dispatches it
5
+ * to the registered analyzers (see `./analyzers/registry.ts`), and feeds
6
+ * the resulting findings into the report generator.
7
+ *
8
+ * Phase 0 refactor:
9
+ * - Resources are dispatched through a plugin-style `Analyzer` registry
10
+ * instead of a hard-coded `switch` statement.
11
+ * - Per-subscription resources are analyzed in parallel via a small
12
+ * in-process limiter (default concurrency 8, configurable via
13
+ * `AzureConfig.concurrency`).
14
+ * - Azure Monitor metric calls are memoized for the duration of a run via
15
+ * a per-run `MetricsCache` passed through `AnalyzerContext`, so concurrent
16
+ * calls to `analyzeAzureResources` stay fully isolated.
17
+ *
18
+ * The output schema (`AzureDetailedResourceReport`) is unchanged so the
19
+ * existing report formats keep working untouched.
3
20
  */
4
21
  import { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
5
22
  import { WebSiteManagementClient } from "@azure/arm-appservice";
@@ -9,73 +26,143 @@ import { NetworkManagementClient } from "@azure/arm-network";
9
26
  import * as armResources from "@azure/arm-resources";
10
27
  import { DefaultAzureCredential } from "@azure/identity";
11
28
  import { getLogger } from "@logtape/logtape";
29
+ import pLimit from "p-limit";
30
+ import { findingsFromAnalysisResult } from "../finding.js";
12
31
  import { DEFAULT_THRESHOLDS, mergeResults, } from "../types.js";
13
- import { generateReport } from "./report.js";
14
- import { analyzeAppServicePlan, analyzeContainerApp, analyzeDisk, analyzeNic, analyzePrivateEndpoint, analyzePublicIp, analyzeStaticSite, analyzeStorageAccount, analyzeVM, } from "./resources/index.js";
32
+ import { createDefaultAnalyzers, createDefaultSubscriptionAnalyzers, } from "./analyzers/index.js";
15
33
  import { matchesTags } from "./utils.js";
34
+ const DEFAULT_CONCURRENCY = 8;
35
+ const DEFAULT_SOURCES = ["advisor", "custom"];
36
+ const RISK_ORDER = { high: 0, low: 2, medium: 1 };
16
37
  /**
17
- * Analyzes resources in multiple Azure subscriptions and generates a report.
38
+ * Analyzes resources in every configured Azure subscription and returns
39
+ * the structured report.
40
+ *
41
+ * Phase 1 change: this function no longer emits a report to stdout. The
42
+ * orchestrator returns `AzureDetailedResourceReport[]` and the caller
43
+ * (the CLI today, future GUI / API consumers tomorrow) chooses how to
44
+ * render it via `generateReport`.
18
45
  *
19
- * @param config - Azure configuration with subscription IDs and settings
20
- * @param format - Output format (table, json, detailed-json, or lint)
46
+ * Each entry carries both the legacy `analysis` summary and the unified
47
+ * `findings: Finding[]` so consumers can pick the level of detail they
48
+ * need. Azure Advisor recommendations and per-resource analyzer outputs
49
+ * are merged into the same entry when they refer to the same resource.
50
+ *
51
+ * @param config - Azure configuration with subscription IDs and settings.
52
+ * `config.sources` controls which analyzers run.
21
53
  */
22
- export async function analyzeAzureResources(config, format) {
54
+ export async function analyzeAzureResources(config) {
23
55
  const logger = getLogger(["savemoney", "azure"]);
24
56
  const credential = new DefaultAzureCredential();
25
57
  const allReports = [];
58
+ const sources = config.sources ?? DEFAULT_SOURCES;
59
+ const customEnabled = sources.includes("custom");
60
+ const advisorEnabled = sources.includes("advisor");
61
+ const analyzers = createDefaultAnalyzers();
62
+ const subscriptionAnalyzers = advisorEnabled
63
+ ? createDefaultSubscriptionAnalyzers()
64
+ : [];
65
+ const thresholds = config.thresholds ?? DEFAULT_THRESHOLDS;
66
+ // Normalise concurrency the same way p-limit does to keep maxInFlight
67
+ // consistent. A raw value of 0/NaN would produce maxInFlight = 0/NaN and
68
+ // either deadlock or silently disable backpressure.
69
+ const rawConcurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
70
+ const concurrency = Number.isFinite(rawConcurrency)
71
+ ? Math.max(1, Math.floor(rawConcurrency))
72
+ : 1;
73
+ const limit = pLimit(concurrency);
74
+ // Bound the in-flight Set to `2 × concurrency` so memory stays proportional
75
+ // to the limiter width, not the total resource count in a subscription.
76
+ const maxInFlight = concurrency * 2;
26
77
  for (const subscriptionId of config.subscriptionIds) {
27
78
  logger.info(`Analyzing subscription: ${subscriptionId}`);
28
- const resourceClient = new armResources.ResourceManagementClient(credential, subscriptionId.trim());
29
- const monitorClient = new MonitorClient(credential, subscriptionId.trim());
30
- const computeClient = new ComputeManagementClient(credential, subscriptionId.trim());
31
- const networkClient = new NetworkManagementClient(credential, subscriptionId.trim());
32
- const webSiteClient = new WebSiteManagementClient(credential, subscriptionId.trim());
33
- const containerAppsClient = new ContainerAppsAPIClient(credential, subscriptionId.trim());
34
- const thresholds = config.thresholds ?? DEFAULT_THRESHOLDS;
35
- // Use the async iterator to avoid memory explosion for large environments
36
- for await (const resource of resourceClient.resources.list()) {
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);
42
- if (suspectedUnused) {
43
- allReports.push({
44
- analysis: {
45
- costRisk,
46
- reason: reason || "No specific findings.",
47
- suspectedUnused,
48
- },
49
- resource: resource,
50
- });
51
- }
79
+ const sid = subscriptionId.trim();
80
+ // Per-subscription index keyed by lowercased resourceId so subscription-
81
+ // level analyzers (Advisor, future quotas, …) can merge their findings
82
+ // back into the matching resource report.
83
+ const reportsById = new Map();
84
+ const taggedResourceIds = new Set();
85
+ // Fresh cache per subscription — bounds peak memory to one subscription's
86
+ // worth of metrics and keeps concurrent analyzeAzureResources calls isolated.
87
+ const runCache = new Map();
88
+ const clients = {
89
+ compute: new ComputeManagementClient(credential, sid),
90
+ containerApps: new ContainerAppsAPIClient(credential, sid),
91
+ monitor: new MonitorClient(credential, sid),
92
+ network: new NetworkManagementClient(credential, sid),
93
+ webSite: new WebSiteManagementClient(credential, sid),
94
+ };
95
+ if (customEnabled) {
96
+ await runPerResourceAnalysis({
97
+ analyzers,
98
+ clients,
99
+ config,
100
+ credential,
101
+ limit,
102
+ logger,
103
+ maxInFlight,
104
+ reports: allReports,
105
+ reportsById,
106
+ runCache,
107
+ sid,
108
+ taggedResourceIds,
109
+ thresholds,
110
+ });
111
+ }
112
+ if (!customEnabled && advisorEnabled && hasTagFilter(config.filterTags)) {
113
+ await collectTaggedResourceIds({
114
+ config,
115
+ credential,
116
+ sid,
117
+ taggedResourceIds,
118
+ });
119
+ }
120
+ if (advisorEnabled && subscriptionAnalyzers.length > 0) {
121
+ await runSubscriptionAnalyzers({
122
+ analyzers: subscriptionAnalyzers,
123
+ credential,
124
+ logger,
125
+ reports: allReports,
126
+ reportsById,
127
+ sid,
128
+ tagFilterActive: hasTagFilter(config.filterTags),
129
+ taggedResourceIds,
130
+ verbose: config.verbose ?? false,
131
+ });
52
132
  }
53
133
  }
54
- // Sort to make the output more readable
134
+ // Sort to make the output more readable:
135
+ // - Subscription-scoped findings (Reserved Instances, savings plans, ...)
136
+ // sink to the bottom: they aggregate many recommendations into a single
137
+ // fat row and are easier to consume after the per-resource rows.
138
+ // - Within each group, sort by cost risk then by resource name.
55
139
  allReports.sort((a, b) => {
140
+ const aSub = isSubscriptionScopedReport(a);
141
+ const bSub = isSubscriptionScopedReport(b);
142
+ if (aSub !== bSub)
143
+ return aSub ? 1 : -1;
56
144
  if (a.analysis.costRisk === b.analysis.costRisk)
57
145
  return (a.resource.name ?? "").localeCompare(b.resource.name ?? "");
58
- const order = { high: 0, low: 2, medium: 1 };
59
- return order[a.analysis.costRisk] - order[b.analysis.costRisk];
146
+ return RISK_ORDER[a.analysis.costRisk] - RISK_ORDER[b.analysis.costRisk];
60
147
  });
61
- await generateReport(allReports, format);
148
+ return allReports;
62
149
  }
63
150
  /**
64
- * Analyzes a single Azure resource based on its type.
151
+ * Analyzes a single Azure resource by dispatching it to every registered
152
+ * analyzer that supports it. Generic checks (missing tags, location
153
+ * mismatch) are applied around the analyzer-specific logic.
65
154
  *
66
- * @param resource - The Azure resource to analyze
67
- * @param monitorClient - Azure Monitor client for metrics
68
- * @param computeClient - Azure Compute client
69
- * @param networkClient - Azure Network client
70
- * @param webSiteClient - Azure Web Site client
71
- * @param containerAppsClient - Azure Container Apps client
72
- * @param preferredLocation - Preferred Azure location
73
- * @param timespanDays - Number of days to analyze metrics
74
- * @param verbose - Whether verbose logging is enabled
155
+ * @param resource The Azure resource to analyze
156
+ * @param analyzers Registered analyzers (typically `createDefaultAnalyzers()`)
157
+ * @param clients Bundle of Azure SDK clients shared across analyzers
158
+ * @param metricsCache Run-scoped metrics cache to pass through to analyzers
159
+ * @param preferredLocation Preferred Azure region (resources elsewhere are flagged)
160
+ * @param timespanDays Look-back window for Azure Monitor metrics
161
+ * @param thresholds Numeric thresholds used during analysis
162
+ * @param verbose Whether verbose logging is enabled
75
163
  * @returns Analysis result with cost risk and reason
76
164
  */
77
- export async function analyzeResource(resource, monitorClient, computeClient, networkClient, webSiteClient, containerAppsClient, preferredLocation, timespanDays, thresholds, verbose = false) {
78
- const type = resource.type?.toLowerCase() || "";
165
+ export async function analyzeResource(resource, analyzers, clients, metricsCache = new Map(), preferredLocation, timespanDays, thresholds, verbose = false) {
79
166
  let result = {
80
167
  costRisk: "low",
81
168
  reason: "",
@@ -86,62 +173,251 @@ export async function analyzeResource(resource, monitorClient, computeClient, ne
86
173
  result.suspectedUnused = true;
87
174
  result.reason += "No tags found. ";
88
175
  }
89
- // Route to type-specific analysis hooks
90
- switch (type) {
91
- case "microsoft.app/containerapps": {
92
- const containerAppResult = await analyzeContainerApp(resource, containerAppsClient, monitorClient, timespanDays, thresholds, verbose);
93
- result = mergeResults(result, containerAppResult);
94
- break;
95
- }
96
- case "microsoft.compute/disks": {
97
- const diskResult = await analyzeDisk(resource, computeClient, verbose);
98
- result = mergeResults(result, diskResult);
99
- break;
176
+ const ctx = {
177
+ clients,
178
+ metricsCache,
179
+ preferredLocation,
180
+ resource,
181
+ thresholds,
182
+ timespanDays,
183
+ verbose,
184
+ };
185
+ let matched = false;
186
+ for (const analyzer of analyzers) {
187
+ if (!analyzer.supports(resource)) {
188
+ continue;
100
189
  }
101
- case "microsoft.compute/virtualmachines": {
102
- const vmResult = await analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds, verbose);
103
- result = mergeResults(result, vmResult);
104
- break;
190
+ matched = true;
191
+ const specific = await analyzer.analyze(ctx);
192
+ result = mergeResults(result, specific);
193
+ }
194
+ if (!matched) {
195
+ result.reason += "No specific analysis for this resource type. ";
196
+ }
197
+ // Generic check for location
198
+ if (resource.location &&
199
+ !resource.location.toLowerCase().includes(preferredLocation.toLowerCase())) {
200
+ result.reason += `Resource not in preferred location (${preferredLocation}). `;
201
+ }
202
+ return { ...result, reason: result.reason.trim() };
203
+ }
204
+ export function shouldIncludeAdvisorFindingForTags(finding, taggedResourceIds, tagFilterActive) {
205
+ if (!tagFilterActive) {
206
+ return true;
207
+ }
208
+ if (finding.source !== "advisor") {
209
+ return true;
210
+ }
211
+ if (!isResourceScopedFinding(finding.resourceId)) {
212
+ // Subscription-level findings are intentionally always global.
213
+ return true;
214
+ }
215
+ return taggedResourceIds.has(normalizeResourceId(finding.resourceId));
216
+ }
217
+ /**
218
+ * Derives a legacy `AnalysisResult` summary from a `Finding`, so the
219
+ * existing report formats keep working untouched on Advisor-only
220
+ * resources.
221
+ */
222
+ function analysisFromFinding(finding) {
223
+ const trimmed = finding.reason.trim();
224
+ const reason = trimmed.endsWith(".") ? trimmed : `${trimmed}.`;
225
+ return {
226
+ costRisk: finding.severity,
227
+ reason,
228
+ suspectedUnused: true,
229
+ };
230
+ }
231
+ /**
232
+ * Builds a minimal `GenericResource` from a resource ID. Used when a
233
+ * subscription-level analyzer surfaces a resource the per-resource pass
234
+ * did not see — we have neither tags nor location, but `name` and `type`
235
+ * can be parsed deterministically from the resource ID structure.
236
+ *
237
+ * Handles three shapes:
238
+ * - Fully qualified: /subscriptions/{sub}/resourceGroups/{rg}/providers/{provider}/{type}/{name}
239
+ * - Resource-group-scoped: /subscriptions/{sub}/resourceGroups/{rg}
240
+ * - Subscription-scoped: /subscriptions/{sub}
241
+ */
242
+ function buildResourceStub(resourceId) {
243
+ const parts = resourceId.split("/").filter((s) => s.length > 0);
244
+ const providersIdx = parts.indexOf("providers");
245
+ if (providersIdx >= 0 && parts.length > providersIdx + 2) {
246
+ // Fully qualified resource ID.
247
+ const provider = parts[providersIdx + 1];
248
+ const tail = parts.slice(providersIdx + 2); // [type, name, subtype, subname, ...]
249
+ const typeSegments = [provider];
250
+ for (let i = 0; i < tail.length; i += 2) {
251
+ typeSegments.push(tail[i]);
105
252
  }
106
- case "microsoft.network/networkinterfaces": {
107
- const nicResult = await analyzeNic(resource, networkClient, verbose);
108
- result = mergeResults(result, nicResult);
109
- break;
253
+ return {
254
+ id: resourceId,
255
+ name: tail[tail.length - 1],
256
+ type: typeSegments.join("/"),
257
+ };
258
+ }
259
+ const rgIdx = parts.indexOf("resourceGroups");
260
+ if (rgIdx >= 0 && parts.length > rgIdx + 1) {
261
+ // Resource-group-scoped ID.
262
+ return {
263
+ id: resourceId,
264
+ name: parts[rgIdx + 1],
265
+ type: "Microsoft.Resources/resourceGroups",
266
+ };
267
+ }
268
+ const subIdx = parts.indexOf("subscriptions");
269
+ if (subIdx >= 0 && parts.length > subIdx + 1) {
270
+ // Subscription-scoped ID (e.g. Reserved Instance recommendations).
271
+ return {
272
+ id: resourceId,
273
+ name: parts[subIdx + 1],
274
+ type: "Microsoft.Subscription",
275
+ };
276
+ }
277
+ // Fallback for completely unknown shapes.
278
+ return { id: resourceId, name: parts[parts.length - 1], type: undefined };
279
+ }
280
+ async function collectTaggedResourceIds(args) {
281
+ const { config, credential, sid, taggedResourceIds } = args;
282
+ const resourceClient = new armResources.ResourceManagementClient(credential, sid);
283
+ for await (const resource of resourceClient.resources.list()) {
284
+ if (!matchesTags(resource, config.filterTags)) {
285
+ continue;
110
286
  }
111
- case "microsoft.network/privateendpoints": {
112
- const peResult = await analyzePrivateEndpoint(resource, networkClient, verbose);
113
- result = mergeResults(result, peResult);
114
- break;
287
+ const resourceId = normalizeResourceId(resource.id);
288
+ if (resourceId) {
289
+ taggedResourceIds.add(resourceId);
115
290
  }
116
- case "microsoft.network/publicipaddresses": {
117
- const pipResult = await analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds, verbose);
118
- result = mergeResults(result, pipResult);
119
- break;
291
+ }
292
+ }
293
+ function hasTagFilter(filterTags) {
294
+ return Boolean(filterTags && filterTags.size > 0);
295
+ }
296
+ function isResourceScopedFinding(resourceId) {
297
+ return /\/providers\//i.test(resourceId);
298
+ }
299
+ function isSubscriptionScopedReport(r) {
300
+ return r.resource.type === "Microsoft.Subscription";
301
+ }
302
+ /**
303
+ * Inserts a `Finding` into the right report entry, creating a stub
304
+ * resource entry on the fly when the finding refers to a resource that
305
+ * the per-resource pass did not analyze.
306
+ */
307
+ function mergeFinding(finding, reports, reportsById) {
308
+ const idKey = finding.resourceId.toLowerCase();
309
+ const existing = reportsById.get(idKey);
310
+ if (existing) {
311
+ existing.findings = [...(existing.findings ?? []), finding];
312
+ const added = analysisFromFinding(finding);
313
+ // Use max costRisk (not last-wins) and join reasons with a space so we
314
+ // don't produce "Sentence one.Sentence two." when the existing reason is
315
+ // already trimmed (i.e. has no trailing separator space).
316
+ existing.analysis = {
317
+ costRisk: RISK_ORDER[existing.analysis.costRisk] <= RISK_ORDER[added.costRisk]
318
+ ? existing.analysis.costRisk
319
+ : added.costRisk,
320
+ reason: existing.analysis.reason && added.reason
321
+ ? `${existing.analysis.reason.trimEnd()} ${added.reason.trimStart()}`
322
+ : existing.analysis.reason || added.reason,
323
+ suspectedUnused: existing.analysis.suspectedUnused || added.suspectedUnused,
324
+ };
325
+ return;
326
+ }
327
+ const stub = buildResourceStub(finding.resourceId);
328
+ const report = {
329
+ analysis: analysisFromFinding(finding),
330
+ findings: [finding],
331
+ resource: stub,
332
+ };
333
+ reports.push(report);
334
+ reportsById.set(idKey, report);
335
+ }
336
+ function normalizeResourceId(resourceId) {
337
+ return (resourceId ?? "").trim().toLowerCase();
338
+ }
339
+ /**
340
+ * Runs the per-resource analyzer plugins against every resource in the
341
+ * given subscription. Extracted from `analyzeAzureResources` to keep that
342
+ * function readable now that subscription-level analyzers were added.
343
+ */
344
+ async function runPerResourceAnalysis(args) {
345
+ const { analyzers, clients, config, credential, limit, logger, maxInFlight, reports, reportsById, runCache, sid, taggedResourceIds, thresholds, } = args;
346
+ const resourceClient = new armResources.ResourceManagementClient(credential, sid);
347
+ const inFlight = new Set();
348
+ // Use the async iterator to avoid loading all resources into memory at once.
349
+ for await (const resource of resourceClient.resources.list()) {
350
+ if (!matchesTags(resource, config.filterTags)) {
351
+ continue;
120
352
  }
121
- case "microsoft.storage/storageaccounts": {
122
- const storageResult = await analyzeStorageAccount(resource, monitorClient, timespanDays, thresholds, verbose);
123
- result = mergeResults(result, storageResult);
124
- break;
353
+ const taggedId = normalizeResourceId(resource.id);
354
+ if (taggedId) {
355
+ taggedResourceIds.add(taggedId);
125
356
  }
126
- case "microsoft.web/serverfarms": {
127
- const aspResult = await analyzeAppServicePlan(resource, webSiteClient, monitorClient, timespanDays, thresholds, verbose);
128
- result = mergeResults(result, aspResult);
129
- break;
357
+ // Backpressure: wait for a slot before enqueuing the next task so that
358
+ // the inFlight Set stays bounded by maxInFlight instead of growing to the
359
+ // total resource count in the subscription.
360
+ while (inFlight.size >= maxInFlight) {
361
+ await Promise.race(inFlight).catch(() => undefined);
130
362
  }
131
- case "microsoft.web/staticsites": {
132
- const staticSiteResult = await analyzeStaticSite(resource, monitorClient, timespanDays, thresholds, verbose);
133
- result = mergeResults(result, staticSiteResult);
134
- break;
363
+ const task = limit(async () => {
364
+ const analysis = await analyzeResource(resource, analyzers, clients, runCache, config.preferredLocation, config.timespanDays, thresholds, config.verbose || false);
365
+ if (analysis.suspectedUnused) {
366
+ const reason = analysis.reason || "No specific findings.";
367
+ const report = {
368
+ analysis: { ...analysis, reason },
369
+ findings: findingsFromAnalysisResult({
370
+ reason,
371
+ resourceId: resource.id ?? "",
372
+ severity: analysis.costRisk,
373
+ source: "custom",
374
+ }),
375
+ resource,
376
+ };
377
+ reports.push(report);
378
+ const idKey = (resource.id ?? "").toLowerCase();
379
+ if (idKey)
380
+ reportsById.set(idKey, report);
381
+ }
382
+ });
383
+ inFlight.add(task);
384
+ // Suppress the unhandled-rejection that would occur between task creation
385
+ // and the Promise.allSettled drain below. The .catch() handler is a no-op
386
+ // because the actual error is still visible to allSettled (which logs it)
387
+ // via the original `task` reference kept in inFlight.
388
+ void task.catch(() => undefined).finally(() => inFlight.delete(task));
389
+ }
390
+ // Drain remaining tasks; surface any unexpected errors so they don't
391
+ // disappear silently and produce an incomplete report without a signal.
392
+ const results = await Promise.allSettled(inFlight);
393
+ for (const result of results) {
394
+ if (result.status === "rejected") {
395
+ logger.error(`Resource analysis failed: ${String(result.reason)}`);
135
396
  }
136
- default:
137
- result.reason += "No specific analysis for this resource type. ";
138
- break;
139
397
  }
140
- // Generic check for location
141
- if (resource.location &&
142
- !resource.location.toLowerCase().includes(preferredLocation.toLowerCase())) {
143
- result.reason += `Resource not in preferred location (${preferredLocation}). `;
398
+ }
399
+ /**
400
+ * Runs every subscription-level analyzer in parallel and merges their
401
+ * findings into the per-resource reports. Findings about resources that
402
+ * the per-resource pass did not surface (typical for Advisor, which
403
+ * reaches SQL DBs, Front Doors, etc.) produce new report entries with a
404
+ * minimal `GenericResource` stub derived from the resource ID.
405
+ */
406
+ async function runSubscriptionAnalyzers(args) {
407
+ const { analyzers, credential, logger, reports, reportsById, sid, tagFilterActive, taggedResourceIds, verbose, } = args;
408
+ const allFindings = await Promise.all(analyzers.map((a) => a
409
+ .analyze({ credential, subscriptionId: sid, verbose })
410
+ .catch((err) => {
411
+ logger.error(`Subscription analyzer ${a.id} failed: ${String(err)}`);
412
+ return [];
413
+ })));
414
+ for (const findings of allFindings) {
415
+ for (const finding of findings) {
416
+ if (!shouldIncludeAdvisorFindingForTags(finding, taggedResourceIds, tagFilterActive)) {
417
+ continue;
418
+ }
419
+ mergeFinding(finding, reports, reportsById);
420
+ }
144
421
  }
145
- return { ...result, reason: result.reason.trim() };
146
422
  }
147
423
  //# sourceMappingURL=analyzer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyzer.js","sourceRoot":"","sources":["../../src/azure/analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Azure Advisor analyzer.
3
+ *
4
+ * Fetches all `Cost` recommendations for the target subscription via the
5
+ * `@azure/arm-advisor` SDK and normalises them into the unified `Finding`
6
+ * model.
7
+ *
8
+ * ## Two kinds of Advisor cost recommendations
9
+ *
10
+ * ### Resource-specific (resourceMetadata.resourceId contains /providers/)
11
+ * Recommendations about a particular Azure resource (e.g. "right-size this
12
+ * VM"). Each is emitted as a separate Finding so it appears next to the
13
+ * resource in the per-resource section of the report. `enrichReason` adds
14
+ * SKU / region / term context from `extendedProperties` so findings for
15
+ * the same recommendation type on different resources are distinguishable.
16
+ *
17
+ * ### Subscription-scoped (no resourceId, or resourceId without /providers/)
18
+ * Reserved Instance and Savings Plan recommendations. The Advisor API
19
+ * returns **one entry per qualifying combination** (term, scope, quantity,
20
+ * etc.) but the Azure Portal **groups them by `recommendationTypeId`** and
21
+ * shows a single entry for the best option — matching the mental model
22
+ * a user has when deciding whether to buy a commitment.
23
+ *
24
+ * Different amounts within the same `recommendationTypeId` represent
25
+ * **mutually exclusive purchase options** (e.g. different DB sizes or
26
+ * quantities); the user would choose one, not buy all. We therefore report
27
+ * the **maximum** savings amount across all unique configurations, matching
28
+ * the portal's behaviour. Scope variants (Shared / Single / ResourceGroup)
29
+ * that carry the same amount are deduplicated so they do not count more
30
+ * than once. True duplicates — recommendations with the same ARM ID —
31
+ * are also removed before taking the maximum.
32
+ */
33
+ import type { SubscriptionAnalyzer, SubscriptionContext } from "./subscription.js";
34
+ /** Minimal client shape used by the analyzer (and injectable in tests). */
35
+ type AdvisorClientLike = {
36
+ recommendations: {
37
+ list(options?: {
38
+ filter?: string;
39
+ }): AsyncIterable<RecommendationInfo>;
40
+ };
41
+ };
42
+ /** Minimal shape of a recommendation entry needed by the helpers. */
43
+ type RecommendationInfo = {
44
+ category?: string;
45
+ extendedProperties?: Record<string, unknown>;
46
+ id?: string;
47
+ impact?: string;
48
+ recommendationTypeId?: string;
49
+ resourceMetadata?: {
50
+ resourceId?: string;
51
+ };
52
+ shortDescription?: {
53
+ problem?: string;
54
+ solution?: string;
55
+ };
56
+ };
57
+ /**
58
+ * Builds the Advisor subscription-level analyzer.
59
+ *
60
+ * @param clientFactory Optional override to inject a mock client in tests.
61
+ * In production the default factory builds a real
62
+ * `AdvisorManagementClient` from the credential.
63
+ */
64
+ export declare function createAdvisorAnalyzer(clientFactory?: {
65
+ build(credential: SubscriptionContext["credential"], subscriptionId: string): AdvisorClientLike;
66
+ }): SubscriptionAnalyzer;
67
+ export {};
68
+ //# sourceMappingURL=advisor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"advisor.d.ts","sourceRoot":"","sources":["../../../src/azure/analyzers/advisor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAOH,OAAO,KAAK,EACV,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,2EAA2E;AAC3E,KAAK,iBAAiB,GAAG;IACvB,eAAe,EAAE;QACf,IAAI,CAAC,OAAO,CAAC,EAAE;YAAE,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,GAAG,aAAa,CAAC,kBAAkB,CAAC,CAAC;KACxE,CAAC;CACH,CAAC;AAEF,qEAAqE;AACrE,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7C,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gBAAgB,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,gBAAgB,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5D,CAAC;AAuBF;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,aAAa,CAAC,EAAE;IACpD,KAAK,CACH,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,EAC7C,cAAc,EAAE,MAAM,GACrB,iBAAiB,CAAC;CACtB,GAAG,oBAAoB,CAwCvB"}