@infograb/docker-slim-advisor 0.1.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/LICENSE +21 -0
- package/README.md +274 -0
- package/build/base-image-db-loader.d.ts +16 -0
- package/build/base-image-db-loader.js +72 -0
- package/build/base-image-db-loader.js.map +1 -0
- package/build/cli.d.ts +47 -0
- package/build/cli.js +142 -0
- package/build/cli.js.map +1 -0
- package/build/data/base-image-db-schema.d.ts +117 -0
- package/build/data/base-image-db-schema.js +108 -0
- package/build/data/base-image-db-schema.js.map +1 -0
- package/build/data/base-image-db.d.ts +21 -0
- package/build/data/base-image-db.js +190 -0
- package/build/data/base-image-db.js.map +1 -0
- package/build/exit-codes.d.ts +25 -0
- package/build/exit-codes.js +31 -0
- package/build/exit-codes.js.map +1 -0
- package/build/formatters/formatter-dispatcher.d.ts +15 -0
- package/build/formatters/formatter-dispatcher.js +27 -0
- package/build/formatters/formatter-dispatcher.js.map +1 -0
- package/build/formatters/json-formatter.d.ts +10 -0
- package/build/formatters/json-formatter.js +52 -0
- package/build/formatters/json-formatter.js.map +1 -0
- package/build/formatters/json-schema.d.ts +57 -0
- package/build/formatters/json-schema.js +13 -0
- package/build/formatters/json-schema.js.map +1 -0
- package/build/formatters/markdown-formatter.d.ts +12 -0
- package/build/formatters/markdown-formatter.js +97 -0
- package/build/formatters/markdown-formatter.js.map +1 -0
- package/build/formatters/terminal-formatter.d.ts +12 -0
- package/build/formatters/terminal-formatter.js +142 -0
- package/build/formatters/terminal-formatter.js.map +1 -0
- package/build/image-size-lookup.d.ts +35 -0
- package/build/image-size-lookup.js +187 -0
- package/build/image-size-lookup.js.map +1 -0
- package/build/multi-stage-detector.d.ts +29 -0
- package/build/multi-stage-detector.js +29 -0
- package/build/multi-stage-detector.js.map +1 -0
- package/build/output.d.ts +46 -0
- package/build/output.js +62 -0
- package/build/output.js.map +1 -0
- package/build/parser.d.ts +7 -0
- package/build/parser.js +123 -0
- package/build/parser.js.map +1 -0
- package/build/rules/alpine-swap.d.ts +10 -0
- package/build/rules/alpine-swap.js +81 -0
- package/build/rules/alpine-swap.js.map +1 -0
- package/build/rules/dockerignore-missing.d.ts +19 -0
- package/build/rules/dockerignore-missing.js +107 -0
- package/build/rules/dockerignore-missing.js.map +1 -0
- package/build/rules/index.d.ts +11 -0
- package/build/rules/index.js +22 -0
- package/build/rules/index.js.map +1 -0
- package/build/rules/package-cache-cleanup.d.ts +15 -0
- package/build/rules/package-cache-cleanup.js +89 -0
- package/build/rules/package-cache-cleanup.js.map +1 -0
- package/build/rules/run-merge.d.ts +12 -0
- package/build/rules/run-merge.js +67 -0
- package/build/rules/run-merge.js.map +1 -0
- package/build/rules/unnecessary-packages.d.ts +23 -0
- package/build/rules/unnecessary-packages.js +184 -0
- package/build/rules/unnecessary-packages.js.map +1 -0
- package/build/severity-filter.d.ts +22 -0
- package/build/severity-filter.js +31 -0
- package/build/severity-filter.js.map +1 -0
- package/build/size-estimator.d.ts +35 -0
- package/build/size-estimator.js +238 -0
- package/build/size-estimator.js.map +1 -0
- package/build/tty-detection.d.ts +20 -0
- package/build/tty-detection.js +33 -0
- package/build/tty-detection.js.map +1 -0
- package/build/types.d.ts +82 -0
- package/build/types.js +6 -0
- package/build/types.js.map +1 -0
- package/package.json +52 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-stage build detection - counts FROM instructions to determine
|
|
3
|
+
* if a Dockerfile uses multi-stage builds. Multi-stage Dockerfiles
|
|
4
|
+
* (2+ FROM instructions) are flagged as "already optimized" since
|
|
5
|
+
* multi-stage is itself a key size optimization technique.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Detect multi-stage builds by counting FROM instructions in the parsed AST.
|
|
9
|
+
* A Dockerfile with 2+ FROM instructions is considered multi-stage and
|
|
10
|
+
* "already optimized" for image size via build stages.
|
|
11
|
+
*/
|
|
12
|
+
export function detectMultiStage(instructions) {
|
|
13
|
+
const fromInstructions = instructions.filter((i) => i.type === 'FROM');
|
|
14
|
+
const stageCount = fromInstructions.length;
|
|
15
|
+
const isMultiStage = stageCount >= 2;
|
|
16
|
+
const stages = fromInstructions.map((f) => ({
|
|
17
|
+
image: f.image,
|
|
18
|
+
tag: f.tag,
|
|
19
|
+
alias: f.alias,
|
|
20
|
+
line: f.line,
|
|
21
|
+
}));
|
|
22
|
+
const message = isMultiStage
|
|
23
|
+
? `Multi-stage build detected (${stageCount} stages) — already optimized for image size.`
|
|
24
|
+
: stageCount === 1
|
|
25
|
+
? 'Single-stage build detected.'
|
|
26
|
+
: 'No FROM instruction found.';
|
|
27
|
+
return { isMultiStage, stageCount, stages, message };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=multi-stage-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multi-stage-detector.js","sourceRoot":"","sources":["../src/multi-stage-detector.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,YAA2B;IAC1D,MAAM,gBAAgB,GAAG,YAAY,CAAC,MAAM,CAC1C,CAAC,CAAC,EAAwB,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAC/C,CAAC;IAEF,MAAM,UAAU,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC3C,MAAM,YAAY,GAAG,UAAU,IAAI,CAAC,CAAC;IAErC,MAAM,MAAM,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1C,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,IAAI,EAAE,CAAC,CAAC,IAAI;KACb,CAAC,CAAC,CAAC;IAEJ,MAAM,OAAO,GAAG,YAAY;QAC1B,CAAC,CAAC,+BAA+B,UAAU,8CAA8C;QACzF,CAAC,CAAC,UAAU,KAAK,CAAC;YAChB,CAAC,CAAC,8BAA8B;YAChC,CAAC,CAAC,4BAA4B,CAAC;IAEnC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized output routing for stderr/stdout separation.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - Structured output (analysis results) -> stdout only
|
|
6
|
+
* - Errors, warnings, diagnostics -> stderr only
|
|
7
|
+
* - Never mix: piping stdout to jq/file must produce clean parseable output
|
|
8
|
+
*
|
|
9
|
+
* This module wraps process.stdout/stderr so the rest of the codebase
|
|
10
|
+
* never calls console.log/console.error directly.
|
|
11
|
+
*/
|
|
12
|
+
/** Writable stream interface for testability (avoids coupling to process) */
|
|
13
|
+
export interface OutputStreams {
|
|
14
|
+
stdout: {
|
|
15
|
+
write(data: string): boolean;
|
|
16
|
+
};
|
|
17
|
+
stderr: {
|
|
18
|
+
write(data: string): boolean;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Override output streams (for testing).
|
|
23
|
+
* Returns a restore function to reset to defaults.
|
|
24
|
+
*/
|
|
25
|
+
export declare function setStreams(streams: OutputStreams): () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Write structured analysis output to stdout.
|
|
28
|
+
* This is the ONLY function that should write to stdout.
|
|
29
|
+
* Used by formatters for terminal, JSON, and markdown output.
|
|
30
|
+
*/
|
|
31
|
+
export declare function writeOutput(data: string): void;
|
|
32
|
+
/**
|
|
33
|
+
* Write an error message to stderr.
|
|
34
|
+
* Used for fatal errors that cause exit code 2.
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeError(message: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* Write a warning/diagnostic to stderr.
|
|
39
|
+
* Used for non-fatal issues (e.g. skipped unparseable lines).
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeWarning(message: string): void;
|
|
42
|
+
/**
|
|
43
|
+
* Write a debug/diagnostic message to stderr.
|
|
44
|
+
* Used for verbose diagnostics when --verbose is enabled.
|
|
45
|
+
*/
|
|
46
|
+
export declare function writeDiagnostic(message: string): void;
|
package/build/output.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized output routing for stderr/stdout separation.
|
|
3
|
+
*
|
|
4
|
+
* Rules:
|
|
5
|
+
* - Structured output (analysis results) -> stdout only
|
|
6
|
+
* - Errors, warnings, diagnostics -> stderr only
|
|
7
|
+
* - Never mix: piping stdout to jq/file must produce clean parseable output
|
|
8
|
+
*
|
|
9
|
+
* This module wraps process.stdout/stderr so the rest of the codebase
|
|
10
|
+
* never calls console.log/console.error directly.
|
|
11
|
+
*/
|
|
12
|
+
/** Default streams pointing to the real process */
|
|
13
|
+
const defaultStreams = {
|
|
14
|
+
stdout: process.stdout,
|
|
15
|
+
stderr: process.stderr,
|
|
16
|
+
};
|
|
17
|
+
/** Active streams — overridable for testing */
|
|
18
|
+
let activeStreams = defaultStreams;
|
|
19
|
+
/**
|
|
20
|
+
* Override output streams (for testing).
|
|
21
|
+
* Returns a restore function to reset to defaults.
|
|
22
|
+
*/
|
|
23
|
+
export function setStreams(streams) {
|
|
24
|
+
activeStreams = streams;
|
|
25
|
+
return () => {
|
|
26
|
+
activeStreams = defaultStreams;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write structured analysis output to stdout.
|
|
31
|
+
* This is the ONLY function that should write to stdout.
|
|
32
|
+
* Used by formatters for terminal, JSON, and markdown output.
|
|
33
|
+
*/
|
|
34
|
+
export function writeOutput(data) {
|
|
35
|
+
activeStreams.stdout.write(data);
|
|
36
|
+
// Ensure trailing newline for clean piping
|
|
37
|
+
if (!data.endsWith('\n')) {
|
|
38
|
+
activeStreams.stdout.write('\n');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Write an error message to stderr.
|
|
43
|
+
* Used for fatal errors that cause exit code 2.
|
|
44
|
+
*/
|
|
45
|
+
export function writeError(message) {
|
|
46
|
+
activeStreams.stderr.write(`error: ${message}\n`);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Write a warning/diagnostic to stderr.
|
|
50
|
+
* Used for non-fatal issues (e.g. skipped unparseable lines).
|
|
51
|
+
*/
|
|
52
|
+
export function writeWarning(message) {
|
|
53
|
+
activeStreams.stderr.write(`warning: ${message}\n`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Write a debug/diagnostic message to stderr.
|
|
57
|
+
* Used for verbose diagnostics when --verbose is enabled.
|
|
58
|
+
*/
|
|
59
|
+
export function writeDiagnostic(message) {
|
|
60
|
+
activeStreams.stderr.write(`${message}\n`);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,mDAAmD;AACnD,MAAM,cAAc,GAAkB;IACpC,MAAM,EAAE,OAAO,CAAC,MAAM;IACtB,MAAM,EAAE,OAAO,CAAC,MAAM;CACvB,CAAC;AAEF,+CAA+C;AAC/C,IAAI,aAAa,GAAkB,cAAc,CAAC;AAElD;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,OAAsB;IAC/C,aAAa,GAAG,OAAO,CAAC;IACxB,OAAO,GAAG,EAAE;QACV,aAAa,GAAG,cAAc,CAAC;IACjC,CAAC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACjC,2CAA2C;IAC3C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,OAAO,IAAI,CAAC,CAAC;AACpD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,OAAO,IAAI,CAAC,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dockerfile parser - converts raw Dockerfile text into typed Instruction AST.
|
|
3
|
+
* Handles multi-line RUN with backslash continuations, comments, and empty lines.
|
|
4
|
+
*/
|
|
5
|
+
import type { Instruction } from './types.js';
|
|
6
|
+
/** Parse raw Dockerfile content into typed Instruction AST */
|
|
7
|
+
export declare function parseDockerfile(content: string): Instruction[];
|
package/build/parser.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dockerfile parser - converts raw Dockerfile text into typed Instruction AST.
|
|
3
|
+
* Handles multi-line RUN with backslash continuations, comments, and empty lines.
|
|
4
|
+
*/
|
|
5
|
+
/** Join lines that end with backslash continuation */
|
|
6
|
+
function joinContinuationLines(lines) {
|
|
7
|
+
const result = [];
|
|
8
|
+
let current = '';
|
|
9
|
+
let startLine = 0;
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
const trimmed = lines[i].trimEnd();
|
|
12
|
+
if (current === '') {
|
|
13
|
+
startLine = i + 1; // 1-indexed line numbers
|
|
14
|
+
}
|
|
15
|
+
if (trimmed.endsWith('\\')) {
|
|
16
|
+
current += (current ? ' ' : '') + trimmed.slice(0, -1).trim();
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
current += (current ? ' ' : '') + trimmed.trim();
|
|
20
|
+
if (current.trim()) {
|
|
21
|
+
result.push({ text: current.trim(), line: startLine });
|
|
22
|
+
}
|
|
23
|
+
current = '';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (current.trim()) {
|
|
27
|
+
result.push({ text: current.trim(), line: startLine });
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/** Parse a FROM instruction */
|
|
32
|
+
function parseFrom(text, line) {
|
|
33
|
+
const stripped = text.replace(/^FROM\s+/i, '').replace(/--platform=\S+\s+/i, '');
|
|
34
|
+
const asMatch = stripped.match(/^(.+?)\s+[Aa][Ss]\s+(\S+)$/);
|
|
35
|
+
const imageStr = asMatch ? asMatch[1].trim() : stripped.trim();
|
|
36
|
+
const alias = asMatch ? asMatch[2] : undefined;
|
|
37
|
+
const [image, tag = 'latest'] = imageStr.split(':');
|
|
38
|
+
return { type: 'FROM', line, raw: text, image, tag, alias };
|
|
39
|
+
}
|
|
40
|
+
/** Parse a RUN instruction */
|
|
41
|
+
function parseRun(text, line, isMultiLine) {
|
|
42
|
+
const command = text.replace(/^RUN\s+/i, '');
|
|
43
|
+
return { type: 'RUN', line, raw: text, command, isMultiLine };
|
|
44
|
+
}
|
|
45
|
+
/** Parse a COPY instruction */
|
|
46
|
+
function parseCopy(text, line) {
|
|
47
|
+
const args = text.replace(/^COPY\s+/i, '');
|
|
48
|
+
const fromMatch = args.match(/--from=(\S+)\s+/);
|
|
49
|
+
const rest = args.replace(/--from=\S+\s+/, '').trim();
|
|
50
|
+
const parts = rest.split(/\s+/);
|
|
51
|
+
const destination = parts.pop() || '';
|
|
52
|
+
const source = parts.join(' ');
|
|
53
|
+
return {
|
|
54
|
+
type: 'COPY', line, raw: text, source, destination,
|
|
55
|
+
...(fromMatch ? { from: fromMatch[1] } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Parse an ADD instruction */
|
|
59
|
+
function parseAdd(text, line) {
|
|
60
|
+
const args = text.replace(/^ADD\s+/i, '').trim();
|
|
61
|
+
const parts = args.split(/\s+/);
|
|
62
|
+
const destination = parts.pop() || '';
|
|
63
|
+
const source = parts.join(' ');
|
|
64
|
+
return { type: 'ADD', line, raw: text, source, destination };
|
|
65
|
+
}
|
|
66
|
+
/** Parse an ENV instruction */
|
|
67
|
+
function parseEnv(text, line) {
|
|
68
|
+
const args = text.replace(/^ENV\s+/i, '');
|
|
69
|
+
const eqMatch = args.match(/^(\S+?)=(.*)$/);
|
|
70
|
+
if (eqMatch) {
|
|
71
|
+
return { type: 'ENV', line, raw: text, key: eqMatch[1], value: eqMatch[2].trim() };
|
|
72
|
+
}
|
|
73
|
+
const parts = args.split(/\s+/, 2);
|
|
74
|
+
return { type: 'ENV', line, raw: text, key: parts[0], value: parts[1] || '' };
|
|
75
|
+
}
|
|
76
|
+
const INSTRUCTION_KEYWORDS = [
|
|
77
|
+
'FROM', 'RUN', 'COPY', 'ADD', 'ENV', 'EXPOSE', 'WORKDIR',
|
|
78
|
+
'CMD', 'ENTRYPOINT', 'LABEL', 'ARG', 'VOLUME', 'USER',
|
|
79
|
+
'HEALTHCHECK', 'SHELL', 'STOPSIGNAL', 'ONBUILD',
|
|
80
|
+
];
|
|
81
|
+
/** Parse raw Dockerfile content into typed Instruction AST */
|
|
82
|
+
export function parseDockerfile(content) {
|
|
83
|
+
const rawLines = content.split('\n');
|
|
84
|
+
const joined = joinContinuationLines(rawLines);
|
|
85
|
+
const instructions = [];
|
|
86
|
+
for (const { text, line } of joined) {
|
|
87
|
+
if (text.startsWith('#') || text === '')
|
|
88
|
+
continue;
|
|
89
|
+
const upperText = text.toUpperCase();
|
|
90
|
+
const keyword = INSTRUCTION_KEYWORDS.find(k => upperText.startsWith(k + ' ') || upperText === k);
|
|
91
|
+
if (!keyword)
|
|
92
|
+
continue; // Gracefully skip unparseable lines
|
|
93
|
+
switch (keyword) {
|
|
94
|
+
case 'FROM':
|
|
95
|
+
instructions.push(parseFrom(text, line));
|
|
96
|
+
break;
|
|
97
|
+
case 'RUN': {
|
|
98
|
+
const originalLine = rawLines[line - 1] || '';
|
|
99
|
+
const isMultiLine = originalLine.trimEnd().endsWith('\\');
|
|
100
|
+
instructions.push(parseRun(text, line, isMultiLine));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'COPY':
|
|
104
|
+
instructions.push(parseCopy(text, line));
|
|
105
|
+
break;
|
|
106
|
+
case 'ADD':
|
|
107
|
+
instructions.push(parseAdd(text, line));
|
|
108
|
+
break;
|
|
109
|
+
case 'ENV':
|
|
110
|
+
instructions.push(parseEnv(text, line));
|
|
111
|
+
break;
|
|
112
|
+
default:
|
|
113
|
+
instructions.push({
|
|
114
|
+
type: keyword,
|
|
115
|
+
line,
|
|
116
|
+
raw: text,
|
|
117
|
+
arguments: text.replace(new RegExp(`^${keyword}\\s*`, 'i'), ''),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return instructions;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alpine/Slim Base Image Swap Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects non-optimized base images (e.g., node:20, python:3.12) and suggests
|
|
5
|
+
* lighter alternatives (slim or alpine variants) with estimated size savings.
|
|
6
|
+
*
|
|
7
|
+
* Severity: HIGH when savings > 50%, MEDIUM when savings > 20%, LOW otherwise.
|
|
8
|
+
*/
|
|
9
|
+
import type { Rule } from '../types.js';
|
|
10
|
+
export declare const alpineSwapRule: Rule;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alpine/Slim Base Image Swap Rule
|
|
3
|
+
*
|
|
4
|
+
* Detects non-optimized base images (e.g., node:20, python:3.12) and suggests
|
|
5
|
+
* lighter alternatives (slim or alpine variants) with estimated size savings.
|
|
6
|
+
*
|
|
7
|
+
* Severity: HIGH when savings > 50%, MEDIUM when savings > 20%, LOW otherwise.
|
|
8
|
+
*/
|
|
9
|
+
import { getSlimAlternative } from '../data/base-image-db.js';
|
|
10
|
+
/** Check if an image tag already indicates a slim/alpine/distroless variant */
|
|
11
|
+
function isAlreadyOptimized(image, tag) {
|
|
12
|
+
const lowerTag = tag.toLowerCase();
|
|
13
|
+
const lowerImage = image.toLowerCase();
|
|
14
|
+
// Alpine or slim tags
|
|
15
|
+
if (lowerTag.includes('alpine') || lowerTag.includes('slim'))
|
|
16
|
+
return true;
|
|
17
|
+
// Distroless images
|
|
18
|
+
if (lowerImage.includes('distroless'))
|
|
19
|
+
return true;
|
|
20
|
+
// Scratch / busybox are minimal
|
|
21
|
+
if (lowerImage === 'scratch' || lowerImage === 'busybox')
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/** Calculate severity based on size reduction percentage */
|
|
26
|
+
function getSeverity(savingsPercent) {
|
|
27
|
+
if (savingsPercent >= 50)
|
|
28
|
+
return 'HIGH';
|
|
29
|
+
if (savingsPercent >= 20)
|
|
30
|
+
return 'MEDIUM';
|
|
31
|
+
return 'LOW';
|
|
32
|
+
}
|
|
33
|
+
/** Format bytes to human-readable string */
|
|
34
|
+
function formatBytes(bytes) {
|
|
35
|
+
if (bytes >= 1_000_000_000)
|
|
36
|
+
return `${(bytes / 1_000_000_000).toFixed(1)}GB`;
|
|
37
|
+
if (bytes >= 1_000_000)
|
|
38
|
+
return `${(bytes / 1_000_000).toFixed(0)}MB`;
|
|
39
|
+
if (bytes >= 1_000)
|
|
40
|
+
return `${(bytes / 1_000).toFixed(0)}KB`;
|
|
41
|
+
return `${bytes}B`;
|
|
42
|
+
}
|
|
43
|
+
export const alpineSwapRule = {
|
|
44
|
+
id: 'DSA001',
|
|
45
|
+
name: 'alpine-slim-swap',
|
|
46
|
+
description: 'Suggests lighter base image alternatives (alpine/slim) to reduce image size',
|
|
47
|
+
evaluate(instructions, context) {
|
|
48
|
+
const recommendations = [];
|
|
49
|
+
// Find all FROM instructions (for single-stage, there should be exactly one)
|
|
50
|
+
const fromInstructions = instructions.filter((i) => i.type === 'FROM');
|
|
51
|
+
for (const from of fromInstructions) {
|
|
52
|
+
// Skip if already using an optimized variant
|
|
53
|
+
if (isAlreadyOptimized(from.image, from.tag))
|
|
54
|
+
continue;
|
|
55
|
+
// Look up current image size
|
|
56
|
+
const currentSize = context.getImageSize(from.image, from.tag);
|
|
57
|
+
if (currentSize === undefined)
|
|
58
|
+
continue; // Unknown image, skip
|
|
59
|
+
// Find a lighter alternative
|
|
60
|
+
const alt = getSlimAlternative(from.image, from.tag);
|
|
61
|
+
if (!alt)
|
|
62
|
+
continue;
|
|
63
|
+
const savingsBytes = currentSize - alt.alternativeSizeBytes;
|
|
64
|
+
if (savingsBytes <= 0)
|
|
65
|
+
continue;
|
|
66
|
+
const savingsPercent = (savingsBytes / currentSize) * 100;
|
|
67
|
+
const severity = getSeverity(savingsPercent);
|
|
68
|
+
recommendations.push({
|
|
69
|
+
ruleId: 'DSA001',
|
|
70
|
+
severity,
|
|
71
|
+
line: from.line,
|
|
72
|
+
title: `Use ${alt.alternative} instead of ${from.image}:${from.tag}`,
|
|
73
|
+
description: `Switching from ${from.image}:${from.tag} (${formatBytes(currentSize)}) to ${alt.alternative} (${formatBytes(alt.alternativeSizeBytes)}) saves ~${formatBytes(savingsBytes)} (${savingsPercent.toFixed(0)}% reduction).`,
|
|
74
|
+
fix: `FROM ${alt.alternative}`,
|
|
75
|
+
estimatedSavingsBytes: savingsBytes,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return recommendations;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=alpine-swap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alpine-swap.js","sourceRoot":"","sources":["../../src/rules/alpine-swap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dockerignore Detection Rule (DSA004)
|
|
3
|
+
*
|
|
4
|
+
* Checks for missing or incomplete .dockerignore file when COPY/ADD
|
|
5
|
+
* instructions use broad source patterns (e.g., "." or "*").
|
|
6
|
+
*
|
|
7
|
+
* Without a proper .dockerignore, COPY . or ADD . will include
|
|
8
|
+
* node_modules, .git, build artifacts, and other unnecessary files,
|
|
9
|
+
* bloating the image by 50-500MB+.
|
|
10
|
+
*
|
|
11
|
+
* Severity: HIGH — .dockerignore is a fundamental Docker best practice.
|
|
12
|
+
* Estimated savings: ~100MB (conservative estimate for typical projects).
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from '../types.js';
|
|
15
|
+
/** Common entries that should be in .dockerignore */
|
|
16
|
+
export declare const RECOMMENDED_ENTRIES: readonly ["node_modules", ".git", "dist", "build", ".env", ".env.*", "*.log", ".DS_Store", "coverage", ".vscode", ".idea", "tmp", "*.md", "docker-compose*.yml", "Dockerfile", ".dockerignore", ".github", "__tests__", "tests", ".nyc_output"];
|
|
17
|
+
/** Find which recommended entries are missing from the .dockerignore content */
|
|
18
|
+
export declare function findMissingEntries(dockerignoreContent: string): string[];
|
|
19
|
+
export declare const dockerignoreMissingRule: Rule;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dockerignore Detection Rule (DSA004)
|
|
3
|
+
*
|
|
4
|
+
* Checks for missing or incomplete .dockerignore file when COPY/ADD
|
|
5
|
+
* instructions use broad source patterns (e.g., "." or "*").
|
|
6
|
+
*
|
|
7
|
+
* Without a proper .dockerignore, COPY . or ADD . will include
|
|
8
|
+
* node_modules, .git, build artifacts, and other unnecessary files,
|
|
9
|
+
* bloating the image by 50-500MB+.
|
|
10
|
+
*
|
|
11
|
+
* Severity: HIGH — .dockerignore is a fundamental Docker best practice.
|
|
12
|
+
* Estimated savings: ~100MB (conservative estimate for typical projects).
|
|
13
|
+
*/
|
|
14
|
+
/** Conservative estimate for unnecessary files copied without .dockerignore (~100MB) */
|
|
15
|
+
const MISSING_DOCKERIGNORE_BYTES = 100 * 1024 * 1024;
|
|
16
|
+
/** Partial .dockerignore — some entries missing (~30MB) */
|
|
17
|
+
const INCOMPLETE_DOCKERIGNORE_BYTES = 30 * 1024 * 1024;
|
|
18
|
+
/** Common entries that should be in .dockerignore */
|
|
19
|
+
export const RECOMMENDED_ENTRIES = [
|
|
20
|
+
'node_modules',
|
|
21
|
+
'.git',
|
|
22
|
+
'dist',
|
|
23
|
+
'build',
|
|
24
|
+
'.env',
|
|
25
|
+
'.env.*',
|
|
26
|
+
'*.log',
|
|
27
|
+
'.DS_Store',
|
|
28
|
+
'coverage',
|
|
29
|
+
'.vscode',
|
|
30
|
+
'.idea',
|
|
31
|
+
'tmp',
|
|
32
|
+
'*.md',
|
|
33
|
+
'docker-compose*.yml',
|
|
34
|
+
'Dockerfile',
|
|
35
|
+
'.dockerignore',
|
|
36
|
+
'.github',
|
|
37
|
+
'__tests__',
|
|
38
|
+
'tests',
|
|
39
|
+
'.nyc_output',
|
|
40
|
+
];
|
|
41
|
+
/** Broad source patterns that copy entire context */
|
|
42
|
+
const BROAD_SOURCE_PATTERNS = ['.', './', '*', './*'];
|
|
43
|
+
/** Check if a COPY/ADD source is a broad pattern that copies the entire build context */
|
|
44
|
+
function isBroadCopySource(source) {
|
|
45
|
+
return BROAD_SOURCE_PATTERNS.includes(source.trim());
|
|
46
|
+
}
|
|
47
|
+
/** Find which recommended entries are missing from the .dockerignore content */
|
|
48
|
+
export function findMissingEntries(dockerignoreContent) {
|
|
49
|
+
const lines = dockerignoreContent
|
|
50
|
+
.split('\n')
|
|
51
|
+
.map(l => l.trim())
|
|
52
|
+
.filter(l => l && !l.startsWith('#'));
|
|
53
|
+
return RECOMMENDED_ENTRIES.filter(entry => {
|
|
54
|
+
// Check if the entry or a glob covering it exists in dockerignore
|
|
55
|
+
return !lines.some(line => {
|
|
56
|
+
// Exact match
|
|
57
|
+
if (line === entry)
|
|
58
|
+
return true;
|
|
59
|
+
// Wildcard match: e.g., "*.log" covers "*.log"
|
|
60
|
+
if (line === entry)
|
|
61
|
+
return true;
|
|
62
|
+
// Directory with trailing slash: "node_modules/" covers "node_modules"
|
|
63
|
+
if (line === `${entry}/`)
|
|
64
|
+
return true;
|
|
65
|
+
// Entry with trailing slash matches line without
|
|
66
|
+
if (`${line}/` === entry || line === entry.replace(/\/$/, ''))
|
|
67
|
+
return true;
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Build the suggested .dockerignore content from missing entries */
|
|
73
|
+
function buildDockerignoreSuggestion(missingEntries) {
|
|
74
|
+
if (missingEntries.length === 0)
|
|
75
|
+
return '';
|
|
76
|
+
return `# Add these entries to .dockerignore:\n${missingEntries.join('\n')}`;
|
|
77
|
+
}
|
|
78
|
+
export const dockerignoreMissingRule = {
|
|
79
|
+
id: 'DSA004',
|
|
80
|
+
name: 'dockerignore-missing',
|
|
81
|
+
description: 'Detects missing or incomplete .dockerignore when broad COPY/ADD patterns are used',
|
|
82
|
+
evaluate(instructions, _context) {
|
|
83
|
+
const recommendations = [];
|
|
84
|
+
// Find COPY/ADD instructions with broad source patterns
|
|
85
|
+
const broadCopies = instructions.filter((i) => (i.type === 'COPY' || i.type === 'ADD') && isBroadCopySource(i.source));
|
|
86
|
+
if (broadCopies.length === 0)
|
|
87
|
+
return recommendations;
|
|
88
|
+
// Use the first broad COPY/ADD line for the recommendation
|
|
89
|
+
const firstBroadCopy = broadCopies[0];
|
|
90
|
+
// We can only do static analysis — we flag the pattern and suggest .dockerignore
|
|
91
|
+
// The rule always fires for broad copies since we cannot read the filesystem
|
|
92
|
+
recommendations.push({
|
|
93
|
+
ruleId: 'DSA004',
|
|
94
|
+
severity: 'HIGH',
|
|
95
|
+
line: firstBroadCopy.line,
|
|
96
|
+
title: 'Broad COPY/ADD without .dockerignore optimization',
|
|
97
|
+
description: `\`${firstBroadCopy.type} ${firstBroadCopy.source}\` copies the entire build context. ` +
|
|
98
|
+
'Without a comprehensive .dockerignore, this includes node_modules, .git, ' +
|
|
99
|
+
'build artifacts, and other files unnecessary in the image. ' +
|
|
100
|
+
'Create or update .dockerignore to exclude development and build files.',
|
|
101
|
+
fix: buildDockerignoreSuggestion([...RECOMMENDED_ENTRIES]),
|
|
102
|
+
estimatedSavingsBytes: MISSING_DOCKERIGNORE_BYTES,
|
|
103
|
+
});
|
|
104
|
+
return recommendations;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=dockerignore-missing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dockerignore-missing.js","sourceRoot":"","sources":["../../src/rules/dockerignore-missing.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry - exports all optimization rules.
|
|
3
|
+
*/
|
|
4
|
+
import type { Rule } from '../types.js';
|
|
5
|
+
/** All registered optimization rules */
|
|
6
|
+
export declare const rules: Rule[];
|
|
7
|
+
export { alpineSwapRule } from './alpine-swap.js';
|
|
8
|
+
export { runMergeRule } from './run-merge.js';
|
|
9
|
+
export { packageCacheCleanupRule } from './package-cache-cleanup.js';
|
|
10
|
+
export { dockerignoreMissingRule } from './dockerignore-missing.js';
|
|
11
|
+
export { unnecessaryPackagesRule } from './unnecessary-packages.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule registry - exports all optimization rules.
|
|
3
|
+
*/
|
|
4
|
+
import { alpineSwapRule } from './alpine-swap.js';
|
|
5
|
+
import { runMergeRule } from './run-merge.js';
|
|
6
|
+
import { packageCacheCleanupRule } from './package-cache-cleanup.js';
|
|
7
|
+
import { dockerignoreMissingRule } from './dockerignore-missing.js';
|
|
8
|
+
import { unnecessaryPackagesRule } from './unnecessary-packages.js';
|
|
9
|
+
/** All registered optimization rules */
|
|
10
|
+
export const rules = [
|
|
11
|
+
alpineSwapRule,
|
|
12
|
+
runMergeRule,
|
|
13
|
+
packageCacheCleanupRule,
|
|
14
|
+
dockerignoreMissingRule,
|
|
15
|
+
unnecessaryPackagesRule,
|
|
16
|
+
];
|
|
17
|
+
export { alpineSwapRule } from './alpine-swap.js';
|
|
18
|
+
export { runMergeRule } from './run-merge.js';
|
|
19
|
+
export { packageCacheCleanupRule } from './package-cache-cleanup.js';
|
|
20
|
+
export { dockerignoreMissingRule } from './dockerignore-missing.js';
|
|
21
|
+
export { unnecessaryPackagesRule } from './unnecessary-packages.js';
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/rules/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,wCAAwC;AACxC,MAAM,CAAC,MAAM,KAAK,GAAW;IAC3B,cAAc;IACd,YAAY;IACZ,uBAAuB;IACvB,uBAAuB;IACvB,uBAAuB;CACxB,CAAC;AAEF,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Cache Cleanup Rule (DSA003)
|
|
3
|
+
*
|
|
4
|
+
* Detects missing cache removal commands in package install layers.
|
|
5
|
+
* Checks for:
|
|
6
|
+
* - apt-get install without `rm -rf /var/lib/apt/lists/*`
|
|
7
|
+
* - apk add without `--no-cache` flag
|
|
8
|
+
*
|
|
9
|
+
* Package manager caches left in a layer bloat the final image by 20-200MB.
|
|
10
|
+
*
|
|
11
|
+
* Severity: HIGH — cache cleanup is a fundamental Docker best practice.
|
|
12
|
+
* Estimated savings: ~40MB for apt, ~10MB for apk.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from '../types.js';
|
|
15
|
+
export declare const packageCacheCleanupRule: Rule;
|