@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/analyzerrc.example.json +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/package.json +81 -0
- package/dist/src/analyzers/image-analyzer.d.ts +8 -0
- package/dist/src/analyzers/image-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/image-analyzer.js +61 -0
- package/dist/src/analyzers/network-analyzer.d.ts +8 -0
- package/dist/src/analyzers/network-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/network-analyzer.js +48 -0
- package/dist/src/analyzers/reliability-analyzer.d.ts +8 -0
- package/dist/src/analyzers/reliability-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/reliability-analyzer.js +84 -0
- package/dist/src/analyzers/resource-analyzer.d.ts +8 -0
- package/dist/src/analyzers/resource-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/resource-analyzer.js +34 -0
- package/dist/src/analyzers/security-analyzer.d.ts +9 -0
- package/dist/src/analyzers/security-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/security-analyzer.js +82 -0
- package/dist/src/cli/options.d.ts +16 -0
- package/dist/src/cli/options.d.ts.map +1 -0
- package/dist/src/cli/options.js +129 -0
- package/dist/src/cli/runner.d.ts +6 -0
- package/dist/src/cli/runner.d.ts.map +1 -0
- package/dist/src/cli/runner.js +234 -0
- package/dist/src/collectors/compose-collector.d.ts +19 -0
- package/dist/src/collectors/compose-collector.d.ts.map +1 -0
- package/dist/src/collectors/compose-collector.js +140 -0
- package/dist/src/collectors/docker-collector.d.ts +8 -0
- package/dist/src/collectors/docker-collector.d.ts.map +1 -0
- package/dist/src/collectors/docker-collector.js +96 -0
- package/dist/src/config/loader.d.ts +3 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +41 -0
- package/dist/src/constants.d.ts +23 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +70 -0
- package/dist/src/health-score.d.ts +3 -0
- package/dist/src/health-score.d.ts.map +1 -0
- package/dist/src/health-score.js +14 -0
- package/dist/src/interactive/display.d.ts +11 -0
- package/dist/src/interactive/display.d.ts.map +1 -0
- package/dist/src/interactive/display.js +117 -0
- package/dist/src/interactive/index.d.ts +15 -0
- package/dist/src/interactive/index.d.ts.map +1 -0
- package/dist/src/interactive/index.js +218 -0
- package/dist/src/interactive/menus.d.ts +68 -0
- package/dist/src/interactive/menus.d.ts.map +1 -0
- package/dist/src/interactive/menus.js +32 -0
- package/dist/src/reporters/diff-reporter.d.ts +21 -0
- package/dist/src/reporters/diff-reporter.d.ts.map +1 -0
- package/dist/src/reporters/diff-reporter.js +87 -0
- package/dist/src/reporters/html-reporter.d.ts +12 -0
- package/dist/src/reporters/html-reporter.d.ts.map +1 -0
- package/dist/src/reporters/html-reporter.js +226 -0
- package/dist/src/reporters/report-generator.d.ts +23 -0
- package/dist/src/reporters/report-generator.d.ts.map +1 -0
- package/dist/src/reporters/report-generator.js +326 -0
- package/dist/src/types.d.ts +198 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/format.d.ts +6 -0
- package/dist/src/utils/format.d.ts.map +1 -0
- package/dist/src/utils/format.js +32 -0
- package/dist/src/utils/print.d.ts +8 -0
- package/dist/src/utils/print.d.ts.map +1 -0
- package/dist/src/utils/print.js +42 -0
- package/package.json +80 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export declare const MAIN_MENU_CHOICES: readonly [{
|
|
2
|
+
readonly name: "🔍 Run analysis";
|
|
3
|
+
readonly value: "analysis";
|
|
4
|
+
}, {
|
|
5
|
+
readonly name: "📊 Generate reports";
|
|
6
|
+
readonly value: "reports";
|
|
7
|
+
}, {
|
|
8
|
+
readonly name: "⚙️ Settings";
|
|
9
|
+
readonly value: "settings";
|
|
10
|
+
}, {
|
|
11
|
+
readonly name: "❌ Exit";
|
|
12
|
+
readonly value: "exit";
|
|
13
|
+
}];
|
|
14
|
+
export declare const ANALYSIS_MENU_CHOICES: readonly [{
|
|
15
|
+
readonly name: "📊 Full analysis";
|
|
16
|
+
readonly value: "full";
|
|
17
|
+
}, {
|
|
18
|
+
readonly name: "⚡ Health";
|
|
19
|
+
readonly value: "health";
|
|
20
|
+
}, {
|
|
21
|
+
readonly name: "🖼️ Images";
|
|
22
|
+
readonly value: "images";
|
|
23
|
+
}, {
|
|
24
|
+
readonly name: "🔐 Security";
|
|
25
|
+
readonly value: "security";
|
|
26
|
+
}, {
|
|
27
|
+
readonly name: "🛟 Reliability";
|
|
28
|
+
readonly value: "reliability";
|
|
29
|
+
}, {
|
|
30
|
+
readonly name: "📦 Resources";
|
|
31
|
+
readonly value: "resources";
|
|
32
|
+
}, {
|
|
33
|
+
readonly name: "🌐 Networks";
|
|
34
|
+
readonly value: "networks";
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "← Back";
|
|
37
|
+
readonly value: "back";
|
|
38
|
+
}];
|
|
39
|
+
export declare const REPORTS_MENU_CHOICES: readonly [{
|
|
40
|
+
readonly name: "📝 Markdown + JSON report";
|
|
41
|
+
readonly value: "markdown";
|
|
42
|
+
}, {
|
|
43
|
+
readonly name: "🌐 Markdown + JSON + HTML report";
|
|
44
|
+
readonly value: "html";
|
|
45
|
+
}, {
|
|
46
|
+
readonly name: "🔀 Diff with previous report";
|
|
47
|
+
readonly value: "diff";
|
|
48
|
+
}, {
|
|
49
|
+
readonly name: "← Back";
|
|
50
|
+
readonly value: "back";
|
|
51
|
+
}];
|
|
52
|
+
export declare const SETTINGS_MENU_CHOICES: readonly [{
|
|
53
|
+
readonly name: "📄 Set compose file";
|
|
54
|
+
readonly value: "compose-file";
|
|
55
|
+
}, {
|
|
56
|
+
readonly name: "📁 Set output directory";
|
|
57
|
+
readonly value: "output";
|
|
58
|
+
}, {
|
|
59
|
+
readonly name: "🐳 Toggle Docker runtime checks";
|
|
60
|
+
readonly value: "with-docker";
|
|
61
|
+
}, {
|
|
62
|
+
readonly name: "📋 Show current settings";
|
|
63
|
+
readonly value: "show";
|
|
64
|
+
}, {
|
|
65
|
+
readonly name: "← Back";
|
|
66
|
+
readonly value: "back";
|
|
67
|
+
}];
|
|
68
|
+
//# sourceMappingURL=menus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../../src/interactive/menus.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAKpB,CAAC;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EASxB,CAAC;AAEX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;EAKvB,CAAC;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;EAMxB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SETTINGS_MENU_CHOICES = exports.REPORTS_MENU_CHOICES = exports.ANALYSIS_MENU_CHOICES = exports.MAIN_MENU_CHOICES = void 0;
|
|
4
|
+
exports.MAIN_MENU_CHOICES = [
|
|
5
|
+
{ name: "🔍 Run analysis", value: "analysis" },
|
|
6
|
+
{ name: "📊 Generate reports", value: "reports" },
|
|
7
|
+
{ name: "⚙️ Settings", value: "settings" },
|
|
8
|
+
{ name: "❌ Exit", value: "exit" },
|
|
9
|
+
];
|
|
10
|
+
exports.ANALYSIS_MENU_CHOICES = [
|
|
11
|
+
{ name: "📊 Full analysis", value: "full" },
|
|
12
|
+
{ name: "⚡ Health", value: "health" },
|
|
13
|
+
{ name: "🖼️ Images", value: "images" },
|
|
14
|
+
{ name: "🔐 Security", value: "security" },
|
|
15
|
+
{ name: "🛟 Reliability", value: "reliability" },
|
|
16
|
+
{ name: "📦 Resources", value: "resources" },
|
|
17
|
+
{ name: "🌐 Networks", value: "networks" },
|
|
18
|
+
{ name: "← Back", value: "back" },
|
|
19
|
+
];
|
|
20
|
+
exports.REPORTS_MENU_CHOICES = [
|
|
21
|
+
{ name: "📝 Markdown + JSON report", value: "markdown" },
|
|
22
|
+
{ name: "🌐 Markdown + JSON + HTML report", value: "html" },
|
|
23
|
+
{ name: "🔀 Diff with previous report", value: "diff" },
|
|
24
|
+
{ name: "← Back", value: "back" },
|
|
25
|
+
];
|
|
26
|
+
exports.SETTINGS_MENU_CHOICES = [
|
|
27
|
+
{ name: "📄 Set compose file", value: "compose-file" },
|
|
28
|
+
{ name: "📁 Set output directory", value: "output" },
|
|
29
|
+
{ name: "🐳 Toggle Docker runtime checks", value: "with-docker" },
|
|
30
|
+
{ name: "📋 Show current settings", value: "show" },
|
|
31
|
+
{ name: "← Back", value: "back" },
|
|
32
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FullComposeReport } from "../types";
|
|
2
|
+
export interface MetricDiff {
|
|
3
|
+
label: string;
|
|
4
|
+
before: string | number;
|
|
5
|
+
after: string | number;
|
|
6
|
+
delta?: number;
|
|
7
|
+
trend: "better" | "worse" | "neutral" | "unchanged";
|
|
8
|
+
}
|
|
9
|
+
export interface ReportDiff {
|
|
10
|
+
currentAt: string;
|
|
11
|
+
previousAt: string;
|
|
12
|
+
timeDelta: string;
|
|
13
|
+
metrics: MetricDiff[];
|
|
14
|
+
newIssues: string[];
|
|
15
|
+
resolvedIssues: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const DiffReporter: {
|
|
18
|
+
diff(current: FullComposeReport, previous: FullComposeReport): ReportDiff;
|
|
19
|
+
print(diff: ReportDiff, write?: (message?: unknown, ...optionalParams: unknown[]) => void): void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=diff-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-reporter.d.ts","sourceRoot":"","sources":["../../../src/reporters/diff-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACpD;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAID,eAAO,MAAM,YAAY;kBACV,iBAAiB,YAAY,iBAAiB,GAAG,UAAU;gBA8DlE,UAAU,UACT,CACN,OAAO,CAAC,EAAE,OAAO,EACjB,GAAG,cAAc,EAAE,OAAO,EAAE,KACxB,IAAI,GACP,IAAI;CAsCP,CAAC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiffReporter = void 0;
|
|
4
|
+
exports.DiffReporter = {
|
|
5
|
+
diff(current, previous) {
|
|
6
|
+
const currentIssues = collectIssues(current);
|
|
7
|
+
const previousIssues = collectIssues(previous);
|
|
8
|
+
return {
|
|
9
|
+
currentAt: toIsoString(current.generatedAt),
|
|
10
|
+
previousAt: toIsoString(previous.generatedAt),
|
|
11
|
+
timeDelta: describeTimeDelta(previous.generatedAt, current.generatedAt),
|
|
12
|
+
metrics: [
|
|
13
|
+
createMetricDiff("Health score", previous.healthScore, current.healthScore, "higher"),
|
|
14
|
+
createMetricDiff("Total issues", previous.metrics.totalIssues, current.metrics.totalIssues, "lower"),
|
|
15
|
+
createMetricDiff("Critical issues", previous.metrics.criticalIssues, current.metrics.criticalIssues, "lower"),
|
|
16
|
+
createMetricDiff("High issues", previous.metrics.highIssues, current.metrics.highIssues, "lower"),
|
|
17
|
+
createMetricDiff("Services", previous.metrics.totalServices, current.metrics.totalServices, "neutral"),
|
|
18
|
+
createMetricDiff("Build services", previous.metrics.servicesWithBuild, current.metrics.servicesWithBuild, "neutral"),
|
|
19
|
+
createMetricDiff("Named volumes", previous.metrics.namedVolumes, current.metrics.namedVolumes, "neutral"),
|
|
20
|
+
],
|
|
21
|
+
newIssues: currentIssues.filter((issue) => !previousIssues.includes(issue)),
|
|
22
|
+
resolvedIssues: previousIssues.filter((issue) => !currentIssues.includes(issue)),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
print(diff, write = console.log) {
|
|
26
|
+
write(`\nReport diff (${diff.previousAt} → ${diff.currentAt}, ${diff.timeDelta})`);
|
|
27
|
+
for (const metric of diff.metrics) {
|
|
28
|
+
const arrow = metric.trend === "better" ? "⬆️" : metric.trend === "worse" ? "⬇️" : "↔️";
|
|
29
|
+
const status = metric.trend === "better"
|
|
30
|
+
? "✓ better"
|
|
31
|
+
: metric.trend === "worse"
|
|
32
|
+
? "✗ worse"
|
|
33
|
+
: metric.trend === "unchanged"
|
|
34
|
+
? "no change"
|
|
35
|
+
: "informational";
|
|
36
|
+
const delta = metric.delta === undefined || metric.delta === 0
|
|
37
|
+
? ""
|
|
38
|
+
: ` (${metric.delta > 0 ? "+" : ""}${formatValue(metric.delta)})`;
|
|
39
|
+
write(`${arrow} ${metric.label.padEnd(16)} ${formatValue(metric.before)} → ${formatValue(metric.after)}${delta} ${status}`);
|
|
40
|
+
}
|
|
41
|
+
if (diff.newIssues.length > 0) {
|
|
42
|
+
write(`⚠️ New issues (${diff.newIssues.length}): ${diff.newIssues.join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
if (diff.resolvedIssues.length > 0) {
|
|
45
|
+
write(`✓ Resolved (${diff.resolvedIssues.length}): ${diff.resolvedIssues.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
function createMetricDiff(label, before, after, direction) {
|
|
50
|
+
const delta = Math.round((after - before) * 100) / 100;
|
|
51
|
+
if (delta === 0) {
|
|
52
|
+
return { label, before, after, delta: 0, trend: "unchanged" };
|
|
53
|
+
}
|
|
54
|
+
if (direction === "neutral") {
|
|
55
|
+
return { label, before, after, delta, trend: "neutral" };
|
|
56
|
+
}
|
|
57
|
+
const improved = (direction === "higher" && delta > 0) ||
|
|
58
|
+
(direction === "lower" && delta < 0);
|
|
59
|
+
return { label, before, after, delta, trend: improved ? "better" : "worse" };
|
|
60
|
+
}
|
|
61
|
+
function collectIssues(report) {
|
|
62
|
+
return report.allIssues.map((issue) => `${issue.service}:${issue.category}:${issue.severity}:${issue.title}`);
|
|
63
|
+
}
|
|
64
|
+
function describeTimeDelta(previousAt, currentAt) {
|
|
65
|
+
const previous = new Date(previousAt);
|
|
66
|
+
const current = new Date(currentAt);
|
|
67
|
+
const deltaMs = Math.max(0, current.getTime() - previous.getTime());
|
|
68
|
+
const deltaMinutes = Math.round(deltaMs / 60000);
|
|
69
|
+
if (deltaMinutes < 60) {
|
|
70
|
+
return `${deltaMinutes || 1} minute${deltaMinutes === 1 ? "" : "s"} apart`;
|
|
71
|
+
}
|
|
72
|
+
const deltaHours = Math.round(deltaMinutes / 60);
|
|
73
|
+
if (deltaHours < 48) {
|
|
74
|
+
return `${deltaHours} hour${deltaHours === 1 ? "" : "s"} apart`;
|
|
75
|
+
}
|
|
76
|
+
const deltaDays = Math.round(deltaHours / 24);
|
|
77
|
+
return `${deltaDays} day${deltaDays === 1 ? "" : "s"} apart`;
|
|
78
|
+
}
|
|
79
|
+
function toIsoString(value) {
|
|
80
|
+
return new Date(value).toISOString();
|
|
81
|
+
}
|
|
82
|
+
function formatValue(value) {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FullComposeReport } from "../types";
|
|
2
|
+
export declare class HtmlReporter {
|
|
3
|
+
static generate(report: FullComposeReport): string;
|
|
4
|
+
private static runtimeBlock;
|
|
5
|
+
private static section;
|
|
6
|
+
private static tableBlock;
|
|
7
|
+
private static summaryCard;
|
|
8
|
+
private static severityBadge;
|
|
9
|
+
private static getHealthBadgeClass;
|
|
10
|
+
private static escapeHtml;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=html-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-reporter.d.ts","sourceRoot":"","sources":["../../../src/reporters/html-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAGlD,qBAAa,YAAY;IACxB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;IA6LlD,OAAO,CAAC,MAAM,CAAC,YAAY;IAwB3B,OAAO,CAAC,MAAM,CAAC,OAAO;IAItB,OAAO,CAAC,MAAM,CAAC,UAAU;IAyBzB,OAAO,CAAC,MAAM,CAAC,WAAW;IAI1B,OAAO,CAAC,MAAM,CAAC,aAAa;IAI5B,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAOlC,OAAO,CAAC,MAAM,CAAC,UAAU;CAQzB"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HtmlReporter = void 0;
|
|
4
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: grouped HTML helpers keep the template readable.
|
|
5
|
+
class HtmlReporter {
|
|
6
|
+
static generate(report) {
|
|
7
|
+
return `<!DOCTYPE html>
|
|
8
|
+
<html lang="en">
|
|
9
|
+
<head>
|
|
10
|
+
<meta charset="utf-8" />
|
|
11
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
12
|
+
<title>Compose Analysis Report</title>
|
|
13
|
+
<style>
|
|
14
|
+
:root {
|
|
15
|
+
--bg: #f8fafc;
|
|
16
|
+
--surface: #ffffff;
|
|
17
|
+
--text: #0f172a;
|
|
18
|
+
--muted: #475569;
|
|
19
|
+
--border: #cbd5e1;
|
|
20
|
+
}
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
:root {
|
|
23
|
+
--bg: #020617;
|
|
24
|
+
--surface: #0f172a;
|
|
25
|
+
--text: #e2e8f0;
|
|
26
|
+
--muted: #94a3b8;
|
|
27
|
+
--border: #334155;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
* { box-sizing: border-box; }
|
|
31
|
+
body {
|
|
32
|
+
margin: 0;
|
|
33
|
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
34
|
+
background: var(--bg);
|
|
35
|
+
color: var(--text);
|
|
36
|
+
line-height: 1.5;
|
|
37
|
+
}
|
|
38
|
+
header, main {
|
|
39
|
+
width: min(1200px, calc(100% - 2rem));
|
|
40
|
+
margin: 0 auto;
|
|
41
|
+
}
|
|
42
|
+
header {
|
|
43
|
+
padding: 2rem 0 1rem;
|
|
44
|
+
}
|
|
45
|
+
nav {
|
|
46
|
+
position: sticky;
|
|
47
|
+
top: 0;
|
|
48
|
+
z-index: 10;
|
|
49
|
+
background: color-mix(in srgb, var(--bg) 92%, transparent);
|
|
50
|
+
backdrop-filter: blur(10px);
|
|
51
|
+
border-bottom: 1px solid var(--border);
|
|
52
|
+
}
|
|
53
|
+
nav ul {
|
|
54
|
+
width: min(1200px, calc(100% - 2rem));
|
|
55
|
+
margin: 0 auto;
|
|
56
|
+
padding: 0.75rem 0;
|
|
57
|
+
list-style: none;
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 1rem;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
}
|
|
62
|
+
nav a {
|
|
63
|
+
text-decoration: none;
|
|
64
|
+
color: var(--muted);
|
|
65
|
+
}
|
|
66
|
+
nav a:hover { color: var(--text); }
|
|
67
|
+
.badge {
|
|
68
|
+
display: inline-flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
border-radius: 999px;
|
|
71
|
+
padding: 0.25rem 0.75rem;
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
font-size: 0.9rem;
|
|
74
|
+
}
|
|
75
|
+
.badge-critical { background: #fee2e2; color: #991b1b; }
|
|
76
|
+
.badge-high { background: #ffedd5; color: #9a3412; }
|
|
77
|
+
.badge-medium { background: #fef9c3; color: #854d0e; }
|
|
78
|
+
.badge-low { background: #dcfce7; color: #166534; }
|
|
79
|
+
.badge-score-ok { background: #dcfce7; color: #166534; }
|
|
80
|
+
.badge-score-warn { background: #fef9c3; color: #854d0e; }
|
|
81
|
+
.badge-score-high { background: #ffedd5; color: #9a3412; }
|
|
82
|
+
.badge-score-critical { background: #fee2e2; color: #991b1b; }
|
|
83
|
+
.summary-grid {
|
|
84
|
+
display: grid;
|
|
85
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
86
|
+
gap: 1rem;
|
|
87
|
+
margin: 1.5rem 0;
|
|
88
|
+
}
|
|
89
|
+
.card, section {
|
|
90
|
+
background: var(--surface);
|
|
91
|
+
border: 1px solid var(--border);
|
|
92
|
+
border-radius: 1rem;
|
|
93
|
+
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
|
94
|
+
}
|
|
95
|
+
.card { padding: 1rem; }
|
|
96
|
+
.card p, .meta { color: var(--muted); }
|
|
97
|
+
section { margin-bottom: 1rem; overflow: hidden; }
|
|
98
|
+
.section-body { padding: 1.25rem; }
|
|
99
|
+
.table-wrap { overflow-x: auto; }
|
|
100
|
+
table {
|
|
101
|
+
width: 100%;
|
|
102
|
+
border-collapse: collapse;
|
|
103
|
+
min-width: 600px;
|
|
104
|
+
}
|
|
105
|
+
th, td {
|
|
106
|
+
text-align: left;
|
|
107
|
+
padding: 0.75rem;
|
|
108
|
+
border-bottom: 1px solid var(--border);
|
|
109
|
+
vertical-align: top;
|
|
110
|
+
}
|
|
111
|
+
tbody tr:nth-child(even) {
|
|
112
|
+
background: color-mix(in srgb, var(--surface) 92%, var(--border));
|
|
113
|
+
}
|
|
114
|
+
ul { margin: 0; padding-left: 1.25rem; }
|
|
115
|
+
code { white-space: pre-wrap; }
|
|
116
|
+
</style>
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
<header>
|
|
120
|
+
<div class="meta">
|
|
121
|
+
<strong>${HtmlReporter.escapeHtml(report.composeFile)}</strong>
|
|
122
|
+
<span>Generated ${HtmlReporter.escapeHtml(new Date(report.generatedAt).toISOString())}</span>
|
|
123
|
+
</div>
|
|
124
|
+
<h1>Compose Analysis Report</h1>
|
|
125
|
+
<span class="badge ${HtmlReporter.getHealthBadgeClass(report.healthScore)}">Health score: ${report.healthScore}/100</span>
|
|
126
|
+
<div class="summary-grid">
|
|
127
|
+
${HtmlReporter.summaryCard("Services", String(report.metrics.totalServices))}
|
|
128
|
+
${HtmlReporter.summaryCard("Issues", String(report.metrics.totalIssues))}
|
|
129
|
+
${HtmlReporter.summaryCard("Critical", String(report.metrics.criticalIssues))}
|
|
130
|
+
${HtmlReporter.summaryCard("High", String(report.metrics.highIssues))}
|
|
131
|
+
${HtmlReporter.summaryCard("Build services", String(report.metrics.servicesWithBuild))}
|
|
132
|
+
${HtmlReporter.summaryCard("Named volumes", String(report.metrics.namedVolumes))}
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
<nav>
|
|
136
|
+
<ul>
|
|
137
|
+
<li><a href="#summary">Summary</a></li>
|
|
138
|
+
<li><a href="#issues">Issues</a></li>
|
|
139
|
+
<li><a href="#recommendations">Recommendations</a></li>
|
|
140
|
+
${report.dockerRuntime ? '<li><a href="#runtime">Runtime</a></li>' : ""}
|
|
141
|
+
</ul>
|
|
142
|
+
</nav>
|
|
143
|
+
<main>
|
|
144
|
+
${HtmlReporter.section("summary", "Summary", HtmlReporter.tableBlock(["Metric", "Value"], [
|
|
145
|
+
["Compose version", report.version ?? "not declared"],
|
|
146
|
+
["Health score", `${report.healthScore}/100`],
|
|
147
|
+
["Total services", String(report.metrics.totalServices)],
|
|
148
|
+
["Total issues", String(report.metrics.totalIssues)],
|
|
149
|
+
["Critical issues", String(report.metrics.criticalIssues)],
|
|
150
|
+
["High issues", String(report.metrics.highIssues)],
|
|
151
|
+
], "No summary data available."))}
|
|
152
|
+
${HtmlReporter.section("issues", `Issues (${report.allIssues.length})`, HtmlReporter.tableBlock(["Severity", "Service", "Category", "Title", "Detail", "Fix"], report.allIssues.map((issue) => [
|
|
153
|
+
HtmlReporter.severityBadge(issue.severity),
|
|
154
|
+
HtmlReporter.escapeHtml(issue.service),
|
|
155
|
+
HtmlReporter.escapeHtml(issue.category),
|
|
156
|
+
HtmlReporter.escapeHtml(issue.title),
|
|
157
|
+
HtmlReporter.escapeHtml(issue.detail),
|
|
158
|
+
HtmlReporter.escapeHtml(issue.fix ?? ""),
|
|
159
|
+
]), "No issues found.", true))}
|
|
160
|
+
${HtmlReporter.section("recommendations", `Recommendations (${report.recommendations.length})`, report.recommendations.length === 0
|
|
161
|
+
? "<p>No recommendations.</p>"
|
|
162
|
+
: `<ul>${report.recommendations
|
|
163
|
+
.map((recommendation) => `<li><strong>${HtmlReporter.escapeHtml(recommendation.priority.toUpperCase())}</strong> — ${HtmlReporter.escapeHtml(recommendation.service)}: ${HtmlReporter.escapeHtml(recommendation.message)}${recommendation.fix ? `<br /><code>${HtmlReporter.escapeHtml(recommendation.fix)}</code>` : ""}</li>`)
|
|
164
|
+
.join("")}</ul>`)}
|
|
165
|
+
${report.dockerRuntime ? HtmlReporter.section("runtime", "Docker Runtime", HtmlReporter.runtimeBlock(report)) : ""}
|
|
166
|
+
</main>
|
|
167
|
+
</body>
|
|
168
|
+
</html>`;
|
|
169
|
+
}
|
|
170
|
+
static runtimeBlock(report) {
|
|
171
|
+
const runtime = report.dockerRuntime;
|
|
172
|
+
if (!runtime) {
|
|
173
|
+
return "<p>Runtime collection disabled.</p>";
|
|
174
|
+
}
|
|
175
|
+
if (!runtime.available) {
|
|
176
|
+
return `<p>${HtmlReporter.escapeHtml(runtime.error ?? "Docker runtime unavailable.")}</p>`;
|
|
177
|
+
}
|
|
178
|
+
return HtmlReporter.tableBlock(["Service", "Container", "State", "Status", "Health", "Image"], runtime.services.map((service) => [
|
|
179
|
+
HtmlReporter.escapeHtml(service.service),
|
|
180
|
+
HtmlReporter.escapeHtml(service.containerName ?? "—"),
|
|
181
|
+
HtmlReporter.escapeHtml(service.state ?? "—"),
|
|
182
|
+
HtmlReporter.escapeHtml(service.status ?? "—"),
|
|
183
|
+
HtmlReporter.escapeHtml(service.health ?? "—"),
|
|
184
|
+
HtmlReporter.escapeHtml(service.image ?? "—"),
|
|
185
|
+
]), "No runtime information collected.");
|
|
186
|
+
}
|
|
187
|
+
static section(id, title, body) {
|
|
188
|
+
return `<section id="${id}"><div class="section-body"><h2>${title}</h2>${body}</div></section>`;
|
|
189
|
+
}
|
|
190
|
+
static tableBlock(headers, rows, emptyMessage, trustCellHtml = false) {
|
|
191
|
+
if (rows.length === 0) {
|
|
192
|
+
return `<p>${HtmlReporter.escapeHtml(emptyMessage)}</p>`;
|
|
193
|
+
}
|
|
194
|
+
return `<div class="table-wrap"><table><thead><tr>${headers
|
|
195
|
+
.map((header) => `<th>${HtmlReporter.escapeHtml(header)}</th>`)
|
|
196
|
+
.join("")}</tr></thead><tbody>${rows
|
|
197
|
+
.map((row) => `<tr>${row
|
|
198
|
+
.map((cell) => `<td>${trustCellHtml ? cell : HtmlReporter.escapeHtml(cell)}</td>`)
|
|
199
|
+
.join("")}</tr>`)
|
|
200
|
+
.join("")}</tbody></table></div>`;
|
|
201
|
+
}
|
|
202
|
+
static summaryCard(label, value) {
|
|
203
|
+
return `<div class="card"><h3>${HtmlReporter.escapeHtml(label)}</h3><p>${HtmlReporter.escapeHtml(value)}</p></div>`;
|
|
204
|
+
}
|
|
205
|
+
static severityBadge(severity) {
|
|
206
|
+
return `<span class="badge badge-${severity}">${HtmlReporter.escapeHtml(severity)}</span>`;
|
|
207
|
+
}
|
|
208
|
+
static getHealthBadgeClass(score) {
|
|
209
|
+
if (score >= 90)
|
|
210
|
+
return "badge-score-ok";
|
|
211
|
+
if (score >= 70)
|
|
212
|
+
return "badge-score-warn";
|
|
213
|
+
if (score >= 50)
|
|
214
|
+
return "badge-score-high";
|
|
215
|
+
return "badge-score-critical";
|
|
216
|
+
}
|
|
217
|
+
static escapeHtml(value) {
|
|
218
|
+
return value
|
|
219
|
+
.replaceAll("&", "&")
|
|
220
|
+
.replaceAll("<", "<")
|
|
221
|
+
.replaceAll(">", ">")
|
|
222
|
+
.replaceAll('"', """)
|
|
223
|
+
.replaceAll("'", "'");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
exports.HtmlReporter = HtmlReporter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnalyzerOptions, FullComposeReport } from "../types";
|
|
2
|
+
export declare class ReportGenerator {
|
|
3
|
+
private readonly outputDir;
|
|
4
|
+
constructor(outputDir?: string, _options?: AnalyzerOptions);
|
|
5
|
+
generateFullReport(report: FullComposeReport, timestamp?: string): Promise<string>;
|
|
6
|
+
generateJsonReport(report: FullComposeReport, timestamp?: string): Promise<string>;
|
|
7
|
+
generateHtmlReport(report: FullComposeReport, timestamp?: string): Promise<string>;
|
|
8
|
+
printSummary(report: FullComposeReport): void;
|
|
9
|
+
private ensureOutputDir;
|
|
10
|
+
private buildMarkdownReport;
|
|
11
|
+
private buildHeader;
|
|
12
|
+
private buildExecutiveSummary;
|
|
13
|
+
private buildMetricsSection;
|
|
14
|
+
private buildIssuesSection;
|
|
15
|
+
private buildImageSection;
|
|
16
|
+
private buildSecuritySection;
|
|
17
|
+
private buildReliabilitySection;
|
|
18
|
+
private buildResourceSection;
|
|
19
|
+
private buildNetworkSection;
|
|
20
|
+
private buildDockerRuntimeSection;
|
|
21
|
+
private buildRecommendationsSection;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=report-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report-generator.d.ts","sourceRoot":"","sources":["../../../src/reporters/report-generator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAWnE,qBAAa,eAAe;IAE1B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,GAAE,MAAoB,EAChD,QAAQ,GAAE,eAAoB;IAGzB,kBAAkB,CACvB,MAAM,EAAE,iBAAiB,EACzB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAUZ,kBAAkB,CACvB,MAAM,EAAE,iBAAiB,EACzB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAUZ,kBAAkB,CACvB,MAAM,EAAE,iBAAiB,EACzB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAUlB,YAAY,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI;YA0B/B,eAAe;IAM7B,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,qBAAqB;IAmC7B,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,kBAAkB;IAiB1B,OAAO,CAAC,iBAAiB;IAkCzB,OAAO,CAAC,oBAAoB;IAqC5B,OAAO,CAAC,uBAAuB;IAgC/B,OAAO,CAAC,oBAAoB;IAiB5B,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,yBAAyB;IAqBjC,OAAO,CAAC,2BAA2B;CAcnC"}
|