@pagopa/dx-savemoney 0.2.6 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +33 -27
  2. package/dist/__tests__/finding.test.d.ts +17 -0
  3. package/dist/__tests__/finding.test.d.ts.map +1 -0
  4. package/dist/__tests__/finding.test.js +124 -0
  5. package/dist/__tests__/finding.test.js.map +1 -0
  6. package/dist/azure/__tests__/analyzer-tags.test.d.ts +8 -0
  7. package/dist/azure/__tests__/analyzer-tags.test.d.ts.map +1 -0
  8. package/dist/azure/__tests__/analyzer-tags.test.js +43 -0
  9. package/dist/azure/__tests__/analyzer-tags.test.js.map +1 -0
  10. package/dist/azure/__tests__/config.test.d.ts +9 -0
  11. package/dist/azure/__tests__/config.test.d.ts.map +1 -0
  12. package/dist/azure/__tests__/config.test.js +70 -0
  13. package/dist/azure/__tests__/config.test.js.map +1 -0
  14. package/dist/azure/__tests__/report.test.d.ts +9 -0
  15. package/dist/azure/__tests__/report.test.d.ts.map +1 -0
  16. package/dist/azure/__tests__/report.test.js +120 -0
  17. package/dist/azure/__tests__/report.test.js.map +1 -0
  18. package/dist/azure/__tests__/utils.test.d.ts +15 -0
  19. package/dist/azure/__tests__/utils.test.d.ts.map +1 -0
  20. package/dist/azure/__tests__/utils.test.js +181 -0
  21. package/dist/azure/__tests__/utils.test.js.map +1 -0
  22. package/dist/azure/analyzer.d.ts +18 -5
  23. package/dist/azure/analyzer.d.ts.map +1 -1
  24. package/dist/azure/analyzer.js +295 -48
  25. package/dist/azure/analyzer.js.map +1 -1
  26. package/dist/azure/analyzers/__tests__/advisor.test.d.ts +9 -0
  27. package/dist/azure/analyzers/__tests__/advisor.test.d.ts.map +1 -0
  28. package/dist/azure/analyzers/__tests__/advisor.test.js +314 -0
  29. package/dist/azure/analyzers/__tests__/advisor.test.js.map +1 -0
  30. package/dist/azure/analyzers/advisor.d.ts +68 -0
  31. package/dist/azure/analyzers/advisor.d.ts.map +1 -0
  32. package/dist/azure/analyzers/advisor.js +234 -0
  33. package/dist/azure/analyzers/advisor.js.map +1 -0
  34. package/dist/azure/analyzers/index.d.ts +3 -1
  35. package/dist/azure/analyzers/index.d.ts.map +1 -1
  36. package/dist/azure/analyzers/index.js +2 -1
  37. package/dist/azure/analyzers/index.js.map +1 -1
  38. package/dist/azure/analyzers/registry.d.ts +8 -0
  39. package/dist/azure/analyzers/registry.d.ts.map +1 -1
  40. package/dist/azure/analyzers/registry.js +10 -0
  41. package/dist/azure/analyzers/registry.js.map +1 -1
  42. package/dist/azure/analyzers/subscription.d.ts +53 -0
  43. package/dist/azure/analyzers/subscription.d.ts.map +1 -0
  44. package/dist/azure/analyzers/subscription.js +18 -0
  45. package/dist/azure/analyzers/subscription.js.map +1 -0
  46. package/dist/azure/config.d.ts.map +1 -1
  47. package/dist/azure/config.js +1 -0
  48. package/dist/azure/config.js.map +1 -1
  49. package/dist/azure/index.d.ts +1 -0
  50. package/dist/azure/index.d.ts.map +1 -1
  51. package/dist/azure/index.js +1 -0
  52. package/dist/azure/index.js.map +1 -1
  53. package/dist/azure/report.d.ts.map +1 -1
  54. package/dist/azure/report.js +178 -29
  55. package/dist/azure/report.js.map +1 -1
  56. package/dist/azure/resources/__tests__/storage.test.d.ts +11 -0
  57. package/dist/azure/resources/__tests__/storage.test.d.ts.map +1 -0
  58. package/dist/azure/resources/__tests__/storage.test.js +99 -0
  59. package/dist/azure/resources/__tests__/storage.test.js.map +1 -0
  60. package/dist/azure/types.d.ts +28 -1
  61. package/dist/azure/types.d.ts.map +1 -1
  62. package/dist/index.d.ts +1 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.test.d.ts +2 -0
  65. package/dist/index.test.d.ts.map +1 -0
  66. package/dist/index.test.js +78 -0
  67. package/dist/index.test.js.map +1 -0
  68. package/dist/schema.d.ts +4 -0
  69. package/dist/schema.d.ts.map +1 -1
  70. package/dist/schema.js +9 -0
  71. package/dist/schema.js.map +1 -1
  72. package/package.json +5 -3
  73. package/src/azure/__tests__/analyzer-tags.test.ts +74 -0
  74. package/src/azure/__tests__/report.test.ts +35 -6
  75. package/src/azure/analyzer.ts +421 -65
  76. package/src/azure/analyzers/__tests__/advisor.test.ts +367 -0
  77. package/src/azure/analyzers/advisor.ts +324 -0
  78. package/src/azure/analyzers/index.ts +9 -1
  79. package/src/azure/analyzers/registry.ts +12 -0
  80. package/src/azure/analyzers/subscription.ts +56 -0
  81. package/src/azure/config.ts +1 -0
  82. package/src/azure/index.ts +1 -0
  83. package/src/azure/report.ts +206 -35
  84. package/src/azure/types.ts +29 -1
  85. package/src/index.ts +1 -1
  86. package/src/schema.ts +9 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/dx-savemoney",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Azure resource analyzer for finding unused or cost-inefficient resources.",
6
6
  "repository": {
@@ -27,6 +27,7 @@
27
27
  "DX"
28
28
  ],
29
29
  "dependencies": {
30
+ "@azure/arm-advisor": "^3.2.0",
30
31
  "@azure/arm-appcontainers": "^3.0.0",
31
32
  "@azure/arm-appservice": "^17.0.0",
32
33
  "@azure/arm-compute": "^23.3.0",
@@ -35,6 +36,7 @@
35
36
  "@azure/arm-resources": "^7.0.0",
36
37
  "@azure/identity": "^4.13.1",
37
38
  "@logtape/logtape": "^1.3.8",
39
+ "cli-table3": "^0.6.5",
38
40
  "js-yaml": "^4.1.1",
39
41
  "p-limit": "^7.3.0",
40
42
  "zod": "^4.4.2"
@@ -43,11 +45,11 @@
43
45
  "@tsconfig/node24": "24.0.4",
44
46
  "@types/js-yaml": "^4.0.9",
45
47
  "@types/node": "^22.19.17",
46
- "@vitest/coverage-v8": "^3.2.4",
48
+ "@vitest/coverage-v8": "^4.1.8",
47
49
  "eslint": "^10.3.0",
48
50
  "prettier": "3.8.3",
49
51
  "typescript": "~5.9.3",
50
- "vitest": "^3.2.4",
52
+ "vitest": "^4.1.8",
51
53
  "@pagopa/eslint-config": "^6.0.4"
52
54
  },
53
55
  "scripts": {
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Unit tests for tag-filter behavior on Advisor findings.
3
+ *
4
+ * These tests exercise the pure helper used by the Azure analyzer
5
+ * orchestrator to keep filtering semantics explicit and stable.
6
+ */
7
+
8
+ import { describe, expect, it } from "vitest";
9
+
10
+ import type { Finding } from "../../finding.js";
11
+
12
+ import { shouldIncludeAdvisorFindingForTags } from "../analyzer.js";
13
+
14
+ function mkFinding(resourceId: string, source: Finding["source"]): Finding {
15
+ return {
16
+ category: "cost",
17
+ code: `${source}.test`,
18
+ reason: "Test finding.",
19
+ resourceId,
20
+ severity: "low",
21
+ source,
22
+ };
23
+ }
24
+
25
+ describe("shouldIncludeAdvisorFindingForTags", () => {
26
+ it("includes all findings when no tag filter is active", () => {
27
+ const finding = mkFinding(
28
+ "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
29
+ "advisor",
30
+ );
31
+
32
+ expect(
33
+ shouldIncludeAdvisorFindingForTags(finding, new Set<string>(), false),
34
+ ).toBe(true);
35
+ });
36
+
37
+ it("keeps subscription-level Advisor findings global even with tag filters", () => {
38
+ const finding = mkFinding("/subscriptions/sub1", "advisor");
39
+
40
+ expect(
41
+ shouldIncludeAdvisorFindingForTags(finding, new Set<string>(), true),
42
+ ).toBe(true);
43
+ });
44
+
45
+ it("includes resource-level Advisor findings only when resource id is tag-matched", () => {
46
+ const finding = mkFinding(
47
+ "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1",
48
+ "advisor",
49
+ );
50
+
51
+ const taggedResourceIds = new Set<string>([
52
+ "/subscriptions/sub1/resourcegroups/rg1/providers/microsoft.compute/virtualmachines/vm1",
53
+ ]);
54
+
55
+ expect(
56
+ shouldIncludeAdvisorFindingForTags(finding, taggedResourceIds, true),
57
+ ).toBe(true);
58
+ });
59
+
60
+ it("excludes resource-level Advisor findings when resource id is not tag-matched", () => {
61
+ const finding = mkFinding(
62
+ "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm2",
63
+ "advisor",
64
+ );
65
+
66
+ const taggedResourceIds = new Set<string>([
67
+ "/subscriptions/sub1/resourcegroups/rg1/providers/microsoft.compute/virtualmachines/vm1",
68
+ ]);
69
+
70
+ expect(
71
+ shouldIncludeAdvisorFindingForTags(finding, taggedResourceIds, true),
72
+ ).toBe(false);
73
+ });
74
+ });
@@ -52,7 +52,7 @@ const LOW_ENTRY = makeEntry(
52
52
  // ── helpers ────────────────────────────────────────────────────────────────
53
53
 
54
54
  function allLogs(spy: ReturnType<typeof vi.spyOn>) {
55
- return spy.mock.calls.map((c) => c[0] as string).join("\n");
55
+ return spy.mock.calls.map((c: string[]) => c[0]).join("\n");
56
56
  }
57
57
 
58
58
  // ── tests ──────────────────────────────────────────────────────────────────
@@ -95,10 +95,8 @@ describe("generateReport — lint format", () => {
95
95
  it("splits a multi-sentence reason into separate findings", async () => {
96
96
  // Reason has two sentences separated by '. '
97
97
  await generateReport([HIGH_ENTRY], "lint");
98
- const calls = logSpy.mock.calls.map(
99
- (c) => (c[0] as string | undefined) ?? "",
100
- );
101
- const findingLines = calls.filter((l) => l.startsWith(" "));
98
+ const calls = logSpy.mock.calls.map((c: string[]) => c[0] ?? "");
99
+ const findingLines = calls.filter((l: string) => l.startsWith(" "));
102
100
  // "VM is deallocated." and "No disk activity detected." → 2 findings
103
101
  expect(findingLines.length).toBe(2);
104
102
  });
@@ -131,8 +129,39 @@ describe("generateReport — lint format", () => {
131
129
 
132
130
  it("prints nothing but the summary for an empty report", async () => {
133
131
  await generateReport([], "lint");
134
- const calls = logSpy.mock.calls.map((c) => c[0] as string);
132
+ const calls = logSpy.mock.calls.map((c: string[]) => c[0]);
135
133
  expect(calls).toHaveLength(1);
136
134
  expect(calls[0]).toContain("0 issues found");
137
135
  });
138
136
  });
137
+
138
+ describe("generateReport — table format", () => {
139
+ let logSpy: ReturnType<typeof vi.spyOn>;
140
+
141
+ beforeEach(() => {
142
+ logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
143
+ });
144
+
145
+ afterEach(() => {
146
+ vi.restoreAllMocks();
147
+ });
148
+
149
+ it("renders without throwing for an empty report", async () => {
150
+ await expect(generateReport([], "table")).resolves.toBeUndefined();
151
+ // Table is always printed; the summary line ("0 issues found") follows.
152
+ expect(logSpy.mock.calls.length).toBeGreaterThanOrEqual(1);
153
+ const output = logSpy.mock.calls
154
+ .map((c: string[]) => String(c[0]))
155
+ .join("\n");
156
+ expect(output).toContain("0 issues found");
157
+ });
158
+
159
+ it("includes resource name and reason in the rendered table", async () => {
160
+ await generateReport([HIGH_ENTRY], "table");
161
+ const output = logSpy.mock.calls
162
+ .map((c: string[]) => String(c[0]))
163
+ .join("\n");
164
+ expect(output).toContain("vm-high");
165
+ expect(output).toContain("VM is deallocated");
166
+ });
167
+ });