@deniscuciuc/compose-analyzer 1.0.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 (72) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +164 -0
  4. package/analyzerrc.example.json +3 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +35 -0
  8. package/dist/package.json +81 -0
  9. package/dist/src/analyzers/image-analyzer.d.ts +8 -0
  10. package/dist/src/analyzers/image-analyzer.d.ts.map +1 -0
  11. package/dist/src/analyzers/image-analyzer.js +61 -0
  12. package/dist/src/analyzers/network-analyzer.d.ts +8 -0
  13. package/dist/src/analyzers/network-analyzer.d.ts.map +1 -0
  14. package/dist/src/analyzers/network-analyzer.js +48 -0
  15. package/dist/src/analyzers/reliability-analyzer.d.ts +8 -0
  16. package/dist/src/analyzers/reliability-analyzer.d.ts.map +1 -0
  17. package/dist/src/analyzers/reliability-analyzer.js +84 -0
  18. package/dist/src/analyzers/resource-analyzer.d.ts +8 -0
  19. package/dist/src/analyzers/resource-analyzer.d.ts.map +1 -0
  20. package/dist/src/analyzers/resource-analyzer.js +34 -0
  21. package/dist/src/analyzers/security-analyzer.d.ts +9 -0
  22. package/dist/src/analyzers/security-analyzer.d.ts.map +1 -0
  23. package/dist/src/analyzers/security-analyzer.js +82 -0
  24. package/dist/src/cli/options.d.ts +16 -0
  25. package/dist/src/cli/options.d.ts.map +1 -0
  26. package/dist/src/cli/options.js +129 -0
  27. package/dist/src/cli/runner.d.ts +6 -0
  28. package/dist/src/cli/runner.d.ts.map +1 -0
  29. package/dist/src/cli/runner.js +234 -0
  30. package/dist/src/collectors/compose-collector.d.ts +19 -0
  31. package/dist/src/collectors/compose-collector.d.ts.map +1 -0
  32. package/dist/src/collectors/compose-collector.js +140 -0
  33. package/dist/src/collectors/docker-collector.d.ts +8 -0
  34. package/dist/src/collectors/docker-collector.d.ts.map +1 -0
  35. package/dist/src/collectors/docker-collector.js +96 -0
  36. package/dist/src/config/loader.d.ts +3 -0
  37. package/dist/src/config/loader.d.ts.map +1 -0
  38. package/dist/src/config/loader.js +41 -0
  39. package/dist/src/constants.d.ts +23 -0
  40. package/dist/src/constants.d.ts.map +1 -0
  41. package/dist/src/constants.js +70 -0
  42. package/dist/src/health-score.d.ts +3 -0
  43. package/dist/src/health-score.d.ts.map +1 -0
  44. package/dist/src/health-score.js +14 -0
  45. package/dist/src/interactive/display.d.ts +11 -0
  46. package/dist/src/interactive/display.d.ts.map +1 -0
  47. package/dist/src/interactive/display.js +117 -0
  48. package/dist/src/interactive/index.d.ts +15 -0
  49. package/dist/src/interactive/index.d.ts.map +1 -0
  50. package/dist/src/interactive/index.js +218 -0
  51. package/dist/src/interactive/menus.d.ts +68 -0
  52. package/dist/src/interactive/menus.d.ts.map +1 -0
  53. package/dist/src/interactive/menus.js +32 -0
  54. package/dist/src/reporters/diff-reporter.d.ts +21 -0
  55. package/dist/src/reporters/diff-reporter.d.ts.map +1 -0
  56. package/dist/src/reporters/diff-reporter.js +87 -0
  57. package/dist/src/reporters/html-reporter.d.ts +12 -0
  58. package/dist/src/reporters/html-reporter.d.ts.map +1 -0
  59. package/dist/src/reporters/html-reporter.js +226 -0
  60. package/dist/src/reporters/report-generator.d.ts +23 -0
  61. package/dist/src/reporters/report-generator.d.ts.map +1 -0
  62. package/dist/src/reporters/report-generator.js +326 -0
  63. package/dist/src/types.d.ts +198 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +2 -0
  66. package/dist/src/utils/format.d.ts +6 -0
  67. package/dist/src/utils/format.d.ts.map +1 -0
  68. package/dist/src/utils/format.js +32 -0
  69. package/dist/src/utils/print.d.ts +8 -0
  70. package/dist/src/utils/print.d.ts.map +1 -0
  71. package/dist/src/utils/print.js +42 -0
  72. package/package.json +80 -0
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SecurityAnalyzer = void 0;
4
+ const compose_collector_1 = require("../collectors/compose-collector");
5
+ const constants_1 = require("../constants");
6
+ class SecurityAnalyzer {
7
+ analyze(services) {
8
+ const secretsInEnv = [];
9
+ const privilegedServices = [];
10
+ const exposedDatabases = [];
11
+ const issues = [];
12
+ for (const { name, service } of services) {
13
+ const envVars = compose_collector_1.ComposeCollector.normalizeEnvironment(service);
14
+ for (const [key, value] of Object.entries(envVars)) {
15
+ const matchedPattern = constants_1.SECRET_PATTERNS.find((pattern) => pattern.test(key));
16
+ if (!matchedPattern ||
17
+ !value.trim() ||
18
+ this.isInterpolatedValue(value)) {
19
+ continue;
20
+ }
21
+ secretsInEnv.push({
22
+ service: name,
23
+ variable: key,
24
+ pattern: matchedPattern.source,
25
+ });
26
+ issues.push({
27
+ service: name,
28
+ severity: "high",
29
+ category: "security",
30
+ title: "Secret value in environment variable",
31
+ detail: `Environment variable "${key}" appears to store a plain-text secret.`,
32
+ fix: "Prefer Docker secrets or external secret injection instead of embedding values in Compose.",
33
+ });
34
+ }
35
+ if (service.privileged) {
36
+ privilegedServices.push({ service: name });
37
+ issues.push({
38
+ service: name,
39
+ severity: "critical",
40
+ category: "security",
41
+ title: "Container runs in privileged mode",
42
+ detail: `Service "${name}" sets privileged: true, which grants broad host access.`,
43
+ fix: "Remove privileged mode and grant only the minimum required capabilities.",
44
+ });
45
+ }
46
+ if (!service.image) {
47
+ continue;
48
+ }
49
+ const baseImage = compose_collector_1.ComposeCollector.baseImageName(service.image);
50
+ if (!constants_1.SENSITIVE_IMAGES.has(baseImage)) {
51
+ continue;
52
+ }
53
+ for (const port of compose_collector_1.ComposeCollector.normalizePorts(service)) {
54
+ if (!port.published) {
55
+ continue;
56
+ }
57
+ if (port.exposesToAllInterfaces) {
58
+ const bindAddr = port.hostIp ?? "0.0.0.0";
59
+ exposedDatabases.push({
60
+ service: name,
61
+ image: service.image,
62
+ port: port.raw,
63
+ bindAddr,
64
+ });
65
+ issues.push({
66
+ service: name,
67
+ severity: "high",
68
+ category: "security",
69
+ title: `Sensitive service port exposed on ${bindAddr}`,
70
+ detail: `${service.image} publishes ${port.raw}, making a database or broker reachable from all interfaces.`,
71
+ fix: `Bind the port to localhost, for example: 127.0.0.1:${port.published}:${port.target}`,
72
+ });
73
+ }
74
+ }
75
+ }
76
+ return { secretsInEnv, privilegedServices, exposedDatabases, issues };
77
+ }
78
+ isInterpolatedValue(value) {
79
+ return /^\$\{[^}]+\}$/.test(value) || /^\$[A-Z0-9_]+$/i.test(value);
80
+ }
81
+ }
82
+ exports.SecurityAnalyzer = SecurityAnalyzer;
@@ -0,0 +1,16 @@
1
+ import { type Command } from "../constants";
2
+ import type { AnalyzerOptions } from "../types";
3
+ export interface ParsedOptions extends AnalyzerOptions {
4
+ composeFile: string;
5
+ compare?: string;
6
+ command: Command;
7
+ config?: string;
8
+ interactive: boolean;
9
+ json: boolean;
10
+ outputDir?: string;
11
+ quiet: boolean;
12
+ version: boolean;
13
+ }
14
+ export declare function parseOptions(argv?: string[]): ParsedOptions;
15
+ export declare function toAnalyzerOptions(options: ParsedOptions): AnalyzerOptions;
16
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../../../src/cli/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,OAAO,EAAY,MAAM,cAAc,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhD,MAAM,WAAW,aAAc,SAAQ,eAAe;IACrD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,YAAY,CAAC,IAAI,WAAwB,GAAG,aAAa,CA0ExE;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,GAAG,eAAe,CAOzE"}
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseOptions = parseOptions;
4
+ exports.toAnalyzerOptions = toAnalyzerOptions;
5
+ const constants_1 = require("../constants");
6
+ function parseOptions(argv = process.argv.slice(2)) {
7
+ const options = {
8
+ composeFile: constants_1.DEFAULTS.composeFile,
9
+ command: "full",
10
+ interactive: false,
11
+ json: false,
12
+ quiet: false,
13
+ version: false,
14
+ withDocker: false,
15
+ html: false,
16
+ };
17
+ for (let index = 0; index < argv.length; index++) {
18
+ const token = argv[index];
19
+ switch (token) {
20
+ case "--file":
21
+ case "-f":
22
+ options.composeFile = requireValue(argv, ++index, token);
23
+ break;
24
+ case "--with-docker":
25
+ options.withDocker = true;
26
+ break;
27
+ case "--compare":
28
+ options.compare = requireValue(argv, ++index, token);
29
+ break;
30
+ case "--html":
31
+ options.html = true;
32
+ break;
33
+ case "--command":
34
+ case "-c": {
35
+ const command = requireValue(argv, ++index, token);
36
+ if (!constants_1.COMMANDS.includes(command)) {
37
+ throw new Error(`Unknown command: ${command}`);
38
+ }
39
+ options.command = command;
40
+ break;
41
+ }
42
+ case "--json":
43
+ case "-j":
44
+ options.json = true;
45
+ break;
46
+ case "--output":
47
+ case "-o":
48
+ options.outputDir = requireValue(argv, ++index, token);
49
+ break;
50
+ case "--interactive":
51
+ case "-i":
52
+ case "start":
53
+ options.interactive = true;
54
+ break;
55
+ case "--config":
56
+ options.config = requireValue(argv, ++index, token);
57
+ break;
58
+ case "--quiet":
59
+ case "-q":
60
+ options.quiet = true;
61
+ break;
62
+ case "--version":
63
+ case "-v":
64
+ options.version = true;
65
+ break;
66
+ case "--help":
67
+ printHelp();
68
+ process.exit(0);
69
+ return options;
70
+ default:
71
+ if (token.startsWith("-")) {
72
+ throw new Error(`Unknown option: ${token}`);
73
+ }
74
+ throw new Error(`Unexpected argument: ${token}`);
75
+ }
76
+ }
77
+ return options;
78
+ }
79
+ function toAnalyzerOptions(options) {
80
+ return {
81
+ outputDir: options.outputDir,
82
+ withDocker: options.withDocker,
83
+ quiet: options.quiet,
84
+ html: options.html,
85
+ };
86
+ }
87
+ function requireValue(argv, index, flag) {
88
+ const value = argv[index];
89
+ if (!value || value.startsWith("-")) {
90
+ throw new Error(`Missing value for ${flag}`);
91
+ }
92
+ return value;
93
+ }
94
+ function printHelp() {
95
+ console.log(`
96
+ Docker Compose Analyzer
97
+ =======================
98
+
99
+ Usage:
100
+ npx @deniscuciuc/compose-analyzer [options]
101
+
102
+ Options:
103
+ -f, --file <path> Path to docker-compose file (default: ${constants_1.DEFAULTS.composeFile})
104
+ --with-docker Enrich with Docker runtime status when the daemon is available
105
+ --compare <path> Compare against a previous JSON report
106
+ --html Also generate an HTML report for full analysis
107
+ -c, --command <command> Command to run (default: full)
108
+ -j, --json Output JSON to stdout
109
+ -o, --output <dir> Output directory for reports (default: ${constants_1.DEFAULTS.output})
110
+ -i, --interactive Interactive menu
111
+ --config <path> Path to a config file (non-connection settings only)
112
+ -q, --quiet Suppress non-essential log output
113
+ -v, --version Print package version
114
+ --help Show this help text
115
+
116
+ Commands:
117
+ full
118
+ health
119
+ images
120
+ security
121
+ reliability
122
+ resources
123
+ networks
124
+
125
+ Notes:
126
+ - This tool is a static file analyzer. There is no watch mode.
127
+ - Config support is limited to non-connection settings such as output.
128
+ `);
129
+ }
@@ -0,0 +1,6 @@
1
+ import type { FullComposeReport } from "../types";
2
+ import type { ParsedOptions } from "./options";
3
+ export declare function buildFullReport(options: ParsedOptions): Promise<FullComposeReport>;
4
+ export declare function executeCommand(options: ParsedOptions): Promise<void>;
5
+ export declare function loadPreviousReport(comparePath: string): FullComposeReport;
6
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/cli/runner.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAgB,iBAAiB,EAAkB,MAAM,UAAU,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,wBAAsB,eAAe,CACpC,OAAO,EAAE,aAAa,GACpB,OAAO,CAAC,iBAAiB,CAAC,CAqD5B;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA6D1E;AAED,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,iBAAiB,CAgBzE"}
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.buildFullReport = buildFullReport;
37
+ exports.executeCommand = executeCommand;
38
+ exports.loadPreviousReport = loadPreviousReport;
39
+ const node_fs_1 = require("node:fs");
40
+ const image_analyzer_1 = require("../analyzers/image-analyzer");
41
+ const network_analyzer_1 = require("../analyzers/network-analyzer");
42
+ const reliability_analyzer_1 = require("../analyzers/reliability-analyzer");
43
+ const resource_analyzer_1 = require("../analyzers/resource-analyzer");
44
+ const security_analyzer_1 = require("../analyzers/security-analyzer");
45
+ const compose_collector_1 = require("../collectors/compose-collector");
46
+ const docker_collector_1 = require("../collectors/docker-collector");
47
+ const constants_1 = require("../constants");
48
+ const health_score_1 = require("../health-score");
49
+ const display = __importStar(require("../interactive/display"));
50
+ const diff_reporter_1 = require("../reporters/diff-reporter");
51
+ const report_generator_1 = require("../reporters/report-generator");
52
+ async function buildFullReport(options) {
53
+ const collector = new compose_collector_1.ComposeCollector(options.composeFile);
54
+ const composeFile = collector.getFile();
55
+ const services = collector.getServices();
56
+ const images = new image_analyzer_1.ImageAnalyzer().analyze(services);
57
+ const security = new security_analyzer_1.SecurityAnalyzer().analyze(services);
58
+ const reliability = new reliability_analyzer_1.ReliabilityAnalyzer().analyze(services);
59
+ const resources = new resource_analyzer_1.ResourceAnalyzer().analyze(services);
60
+ const networks = new network_analyzer_1.NetworkAnalyzer().analyze(services, composeFile.networks);
61
+ const allIssues = sortIssues([
62
+ ...images.issues,
63
+ ...security.issues,
64
+ ...reliability.issues,
65
+ ...resources.issues,
66
+ ...networks.issues,
67
+ ]);
68
+ const metrics = {
69
+ totalServices: services.length,
70
+ servicesWithBuild: services.filter(({ service }) => Boolean(service.build))
71
+ .length,
72
+ namedVolumes: Object.keys(composeFile.volumes ?? {}).length,
73
+ totalIssues: allIssues.length,
74
+ criticalIssues: allIssues.filter((issue) => issue.severity === "critical")
75
+ .length,
76
+ highIssues: allIssues.filter((issue) => issue.severity === "high").length,
77
+ };
78
+ const report = {
79
+ generatedAt: new Date(),
80
+ composeFile: collector.getFilePath(),
81
+ version: composeFile.version,
82
+ healthScore: (0, health_score_1.computeHealthScore)(allIssues, services.length),
83
+ metrics,
84
+ images,
85
+ security,
86
+ reliability,
87
+ resources,
88
+ networks,
89
+ allIssues,
90
+ recommendations: buildRecommendations(allIssues),
91
+ };
92
+ if (options.withDocker) {
93
+ report.dockerRuntime = await new docker_collector_1.DockerCollector().collect(services);
94
+ }
95
+ return report;
96
+ }
97
+ async function executeCommand(options) {
98
+ const log = options.quiet || options.json ? () => { } : console.log;
99
+ const report = await buildFullReport(options);
100
+ if (options.command !== "full") {
101
+ const result = buildCommandResult(report, options.command);
102
+ if (options.json) {
103
+ console.log(JSON.stringify(result, null, 2));
104
+ return;
105
+ }
106
+ renderCommand(report, options.command);
107
+ return;
108
+ }
109
+ if (options.compare) {
110
+ const previous = loadPreviousReport(options.compare);
111
+ diff_reporter_1.DiffReporter.print(diff_reporter_1.DiffReporter.diff(report, previous), options.json ? console.error : console.log);
112
+ }
113
+ if (options.json) {
114
+ console.log(JSON.stringify({
115
+ success: true,
116
+ report,
117
+ summary: {
118
+ healthScore: report.healthScore,
119
+ totalServices: report.metrics.totalServices,
120
+ totalIssues: report.metrics.totalIssues,
121
+ criticalIssues: report.metrics.criticalIssues,
122
+ highIssues: report.metrics.highIssues,
123
+ },
124
+ recommendations: report.recommendations,
125
+ }, null, 2));
126
+ return;
127
+ }
128
+ const reporter = new report_generator_1.ReportGenerator(options.outputDir, options);
129
+ reporter.printSummary(report);
130
+ log("\nGenerating reports...");
131
+ const [markdown, json, html] = await Promise.all([
132
+ reporter.generateFullReport(report),
133
+ reporter.generateJsonReport(report),
134
+ options.html ? reporter.generateHtmlReport(report) : undefined,
135
+ ]);
136
+ log("\nReports generated:");
137
+ log(` - Markdown: ${markdown}`);
138
+ log(` - JSON: ${json}`);
139
+ if (html) {
140
+ log(` - HTML: ${html}`);
141
+ }
142
+ }
143
+ function loadPreviousReport(comparePath) {
144
+ if (!(0, node_fs_1.existsSync)(comparePath)) {
145
+ throw new Error(`Compare report not found: ${comparePath}`);
146
+ }
147
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(comparePath, "utf-8"));
148
+ if (typeof parsed === "object" &&
149
+ parsed !== null &&
150
+ "report" in parsed &&
151
+ parsed.report) {
152
+ return parsed.report;
153
+ }
154
+ return parsed;
155
+ }
156
+ function buildRecommendations(issues) {
157
+ const seen = new Set();
158
+ const recommendations = [];
159
+ for (const issue of issues) {
160
+ const key = `${issue.service}:${issue.title}`;
161
+ if (seen.has(key)) {
162
+ continue;
163
+ }
164
+ seen.add(key);
165
+ recommendations.push({
166
+ priority: issue.severity,
167
+ service: issue.service,
168
+ message: `${issue.title} — ${issue.detail}`,
169
+ fix: issue.fix,
170
+ });
171
+ }
172
+ return recommendations;
173
+ }
174
+ function sortIssues(issues) {
175
+ return [...issues].sort((left, right) => {
176
+ const severityDelta = constants_1.SEVERITY_ORDER[left.severity] - constants_1.SEVERITY_ORDER[right.severity];
177
+ if (severityDelta !== 0) {
178
+ return severityDelta;
179
+ }
180
+ const serviceDelta = left.service.localeCompare(right.service);
181
+ if (serviceDelta !== 0) {
182
+ return serviceDelta;
183
+ }
184
+ return left.title.localeCompare(right.title);
185
+ });
186
+ }
187
+ function buildCommandResult(report, command) {
188
+ switch (command) {
189
+ case "health":
190
+ return {
191
+ healthScore: report.healthScore,
192
+ metrics: report.metrics,
193
+ issues: report.allIssues.map((issue) => `[${issue.severity}] ${issue.service}: ${issue.title}`),
194
+ recommendations: report.recommendations,
195
+ };
196
+ case "images":
197
+ return report.images;
198
+ case "security":
199
+ return report.security;
200
+ case "reliability":
201
+ return report.reliability;
202
+ case "resources":
203
+ return report.resources;
204
+ case "networks":
205
+ return report.networks;
206
+ default:
207
+ return report;
208
+ }
209
+ }
210
+ function renderCommand(report, command) {
211
+ switch (command) {
212
+ case "health":
213
+ display.showHealth(report);
214
+ break;
215
+ case "images":
216
+ display.showImages(report.images);
217
+ break;
218
+ case "security":
219
+ display.showSecurity(report.security);
220
+ break;
221
+ case "reliability":
222
+ display.showReliability(report.reliability);
223
+ break;
224
+ case "resources":
225
+ display.showResources(report.resources);
226
+ break;
227
+ case "networks":
228
+ display.showNetworks(report.networks);
229
+ break;
230
+ case "full":
231
+ display.showFullReportSummary(report);
232
+ break;
233
+ }
234
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComposeFile, ComposeService, NormalizedPort } from "../types";
2
+ export declare class ComposeCollector {
3
+ private readonly parsed;
4
+ private readonly filePath;
5
+ constructor(filePath: string);
6
+ getFile(): ComposeFile;
7
+ getFilePath(): string;
8
+ getServices(): Array<{
9
+ name: string;
10
+ service: ComposeService;
11
+ }>;
12
+ static baseImageName(image: string): string;
13
+ static imageTag(image: string): string;
14
+ static hasDigest(image: string): boolean;
15
+ static normalizeEnvironment(service: ComposeService): Record<string, string>;
16
+ static normalizePorts(service: ComposeService): NormalizedPort[];
17
+ private static parsePortString;
18
+ }
19
+ //# sourceMappingURL=compose-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compose-collector.d.ts","sourceRoot":"","sources":["../../../src/collectors/compose-collector.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE5E,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;gBAEtB,QAAQ,EAAE,MAAM;IAyB5B,OAAO,IAAI,WAAW;IAItB,WAAW,IAAI,MAAM;IAIrB,WAAW,IAAI,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC;IAU/D,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAU3C,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAWtC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO;IAIxC,MAAM,CAAC,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAqB5E,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,EAAE;IAqBhE,OAAO,CAAC,MAAM,CAAC,eAAe;CAuC9B"}
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ComposeCollector = void 0;
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ class ComposeCollector {
11
+ parsed;
12
+ filePath;
13
+ constructor(filePath) {
14
+ const resolved = (0, node_path_1.resolve)(filePath);
15
+ if (!(0, node_fs_1.existsSync)(resolved)) {
16
+ throw new Error(`Compose file not found: ${resolved}`);
17
+ }
18
+ this.filePath = resolved;
19
+ try {
20
+ const loaded = js_yaml_1.default.load((0, node_fs_1.readFileSync)(resolved, "utf-8"));
21
+ if (!loaded || typeof loaded !== "object") {
22
+ throw new Error("Compose file is empty or invalid.");
23
+ }
24
+ if (!loaded.services || typeof loaded.services !== "object") {
25
+ throw new Error('No "services" key found in compose file.');
26
+ }
27
+ this.parsed = loaded;
28
+ }
29
+ catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ throw new Error(`Error parsing ${resolved}: ${message}`);
32
+ }
33
+ }
34
+ getFile() {
35
+ return this.parsed;
36
+ }
37
+ getFilePath() {
38
+ return this.filePath;
39
+ }
40
+ getServices() {
41
+ return Object.entries(this.parsed.services).map(([name, service]) => ({
42
+ name,
43
+ service: {
44
+ ...(service ?? {}),
45
+ name,
46
+ },
47
+ }));
48
+ }
49
+ static baseImageName(image) {
50
+ const reference = image.split("@")[0] ?? image;
51
+ const lastSlash = reference.lastIndexOf("/");
52
+ const lastColon = reference.lastIndexOf(":");
53
+ const withoutTag = lastColon > lastSlash ? reference.slice(0, lastColon) : reference;
54
+ const parts = withoutTag.split("/");
55
+ return (parts[parts.length - 1] ?? withoutTag).toLowerCase();
56
+ }
57
+ static imageTag(image) {
58
+ if (ComposeCollector.hasDigest(image)) {
59
+ return "";
60
+ }
61
+ const reference = image.split("@")[0] ?? image;
62
+ const lastSlash = reference.lastIndexOf("/");
63
+ const lastColon = reference.lastIndexOf(":");
64
+ return lastColon > lastSlash ? reference.slice(lastColon + 1) : "";
65
+ }
66
+ static hasDigest(image) {
67
+ return image.includes("@sha256:");
68
+ }
69
+ static normalizeEnvironment(service) {
70
+ const result = {};
71
+ if (!service.environment) {
72
+ return result;
73
+ }
74
+ if (Array.isArray(service.environment)) {
75
+ for (const entry of service.environment) {
76
+ const [key, ...rest] = entry.split("=");
77
+ result[key] = rest.join("=");
78
+ }
79
+ return result;
80
+ }
81
+ for (const [key, value] of Object.entries(service.environment)) {
82
+ result[key] = value ?? "";
83
+ }
84
+ return result;
85
+ }
86
+ static normalizePorts(service) {
87
+ const ports = service.ports ?? [];
88
+ return ports.map((port) => {
89
+ if (typeof port === "string") {
90
+ return ComposeCollector.parsePortString(port);
91
+ }
92
+ return {
93
+ raw: `${port.host_ip ? `${port.host_ip}:` : ""}${port.published ? `${port.published}:` : ""}${port.target}${port.protocol ? `/${port.protocol}` : ""}`,
94
+ hostIp: port.host_ip,
95
+ published: port.published?.toString(),
96
+ target: port.target.toString(),
97
+ protocol: port.protocol ?? "tcp",
98
+ exposesToAllInterfaces: port.published !== undefined &&
99
+ (!port.host_ip || port.host_ip === "0.0.0.0"),
100
+ randomHostBinding: port.published === undefined,
101
+ };
102
+ });
103
+ }
104
+ static parsePortString(value) {
105
+ const [body, protocol = "tcp"] = value.split("/");
106
+ const parts = body.split(":");
107
+ if (parts.length === 1) {
108
+ return {
109
+ raw: value,
110
+ target: parts[0],
111
+ protocol,
112
+ exposesToAllInterfaces: true,
113
+ randomHostBinding: true,
114
+ };
115
+ }
116
+ if (parts.length === 2) {
117
+ return {
118
+ raw: value,
119
+ published: parts[0],
120
+ target: parts[1],
121
+ protocol,
122
+ exposesToAllInterfaces: true,
123
+ randomHostBinding: false,
124
+ };
125
+ }
126
+ const hostIp = parts.slice(0, -2).join(":");
127
+ const published = parts.at(-2) ?? "";
128
+ const target = parts.at(-1) ?? "";
129
+ return {
130
+ raw: value,
131
+ hostIp,
132
+ published,
133
+ target,
134
+ protocol,
135
+ exposesToAllInterfaces: hostIp === "" || hostIp === "0.0.0.0",
136
+ randomHostBinding: false,
137
+ };
138
+ }
139
+ }
140
+ exports.ComposeCollector = ComposeCollector;
@@ -0,0 +1,8 @@
1
+ import type { ComposeService, DockerRuntimeSummary } from "../types";
2
+ export declare class DockerCollector {
3
+ collect(services: Array<{
4
+ name: string;
5
+ service: ComposeService;
6
+ }>): Promise<DockerRuntimeSummary>;
7
+ }
8
+ //# sourceMappingURL=docker-collector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docker-collector.d.ts","sourceRoot":"","sources":["../../../src/collectors/docker-collector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,cAAc,EAEd,oBAAoB,EACpB,MAAM,UAAU,CAAC;AAElB,qBAAa,eAAe;IACrB,OAAO,CACZ,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,cAAc,CAAA;KAAE,CAAC,GACxD,OAAO,CAAC,oBAAoB,CAAC;CA6DhC"}