vibe-guard
Version:
██ Vibe-Guard Security Scanner - 28 essential security rules to catch vulnerabilities before they catch you! Zero dependencies, instant setup, works everywhere, optimized performance. Detects SQL injection, XSS, exposed secrets, CSRF, CORS issues, contain
280 lines • 15.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DockerfileSecurityRule = void 0;
const types_1 = require("../types");
class DockerfileSecurityRule extends types_1.BaseRule {
constructor() {
super(...arguments);
this.name = 'dockerfile-security';
this.description = 'Detects common Dockerfile security vulnerabilities and misconfigurations';
this.severity = 'high';
this.dockerfilePatterns = [
{
pattern: /^FROM\s+([^\s]+)(?!@sha256:)/gm,
type: 'Missing Image Digest',
severity: 'high',
description: 'Image reference without @sha256 digest can lead to supply chain attacks',
suggestion: 'Use pinned digest: FROM node:18.17.0-alpine@sha256:abc123...'
},
{
pattern: /^FROM\s+([^\s:]+):latest/gm,
type: 'Latest Tag Usage',
severity: 'high',
description: 'Using latest tag can lead to unpredictable builds and security vulnerabilities',
suggestion: 'Use specific version tags (e.g., node:18.17.0-alpine instead of node:latest)'
},
{
pattern: /^USER\s+root/gm,
type: 'Root User Execution',
severity: 'critical',
description: 'Running container as root user creates security risks',
suggestion: 'Create and use a non-root user: RUN adduser --disabled-password --gecos "" appuser && USER appuser'
},
{
pattern: /^EXPOSE\s+(\d+)/gm,
type: 'Port Exposure',
severity: 'medium',
description: 'Exposed ports without justification or security context',
suggestion: 'Only expose necessary ports and consider using non-standard ports for internal services'
},
{
pattern: /^RUN\s+.*(?:apt-get|yum|apk)\s+install.*(?:-y|--yes)/gm,
type: 'Package Installation',
severity: 'medium',
description: 'Package installation without cleanup can increase image size and attack surface',
suggestion: 'Clean up package caches and pin versions: RUN apt-get update && apt-get install -y package=1.2.3 && rm -rf /var/lib/apt/lists/*'
},
{
pattern: /^COPY\s+.*\s+\/$/gm,
type: 'Copy to Root',
severity: 'high',
description: 'Copying files to root directory can lead to permission issues',
suggestion: 'Copy to specific directories and set appropriate ownership'
},
{
pattern: /^ADD\s+/gm,
type: 'ADD Directive Usage',
severity: 'medium',
description: 'ADD directive can extract archives and download from URLs, creating security risks',
suggestion: 'Use COPY for local files, download and verify external resources separately. Add .dockerignore to prevent leaks'
},
{
pattern: /^ENV\s+.*(?:PASSWORD|SECRET|KEY|TOKEN)\s*=/gmi,
type: 'Sensitive Environment Variables',
severity: 'critical',
description: 'Sensitive data in environment variables can be exposed',
suggestion: 'Use Docker secrets or external secret management systems'
},
{
pattern: /^RUN\s+.*(?:curl|wget)\s+.*(?!--checksum|--hash)/gm,
type: 'Insecure Download',
severity: 'high',
description: 'Downloads without checksum validation can lead to supply chain attacks',
suggestion: 'Validate checksums: RUN curl -L --checksum sha256:abc123... https://example.com/file'
},
{
pattern: /^RUN\s+(?!.*&&).*$/gm,
type: 'Unchained RUN Commands',
severity: 'medium',
description: 'RUN commands without && chaining can hide failures and create security risks',
suggestion: 'Chain commands with &&: RUN apt-get update && apt-get install -y package && rm -rf /var/lib/apt/lists/*'
},
{
pattern: /^HEALTHCHECK\s+NONE/gm,
type: 'Disabled Healthcheck',
severity: 'medium',
description: 'Healthcheck disabled or set to NONE',
suggestion: 'Enable healthcheck: HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl -f http://localhost:3000/health || exit 1'
},
{
pattern: /^FROM\s+(?:ubuntu|debian|centos|rhel)(?!.*alpine)/gm,
type: 'Large Base Image',
severity: 'low',
description: 'Using large base images increases attack surface and image size',
suggestion: 'Consider minimal variants: alpine, distroless, or scratch for smaller, more secure images'
}
];
}
check(fileContent) {
const issues = [];
if (!this.isDockerfile(fileContent.path)) {
return issues;
}
const context = this.analyzeDockerfileContext(fileContent.content);
for (const { pattern, type, severity, description, suggestion } of this.dockerfilePatterns) {
const matches = this.findMatches(fileContent.content, pattern);
for (const { line, column, lineContent } of matches) {
if (this.shouldSkipMatch(type, context)) {
continue;
}
const finalSeverity = this.determineSeverity(severity, context, type);
const enhancedSuggestion = this.enhanceSuggestion(suggestion, context, type);
issues.push(this.createIssue(fileContent.path, line, column, lineContent, `${type}: ${description}`, enhancedSuggestion, finalSeverity));
}
}
const missingPractices = this.checkMissingPractices(context);
issues.push(...missingPractices);
return issues;
}
isDockerfile(filePath) {
const lowerPath = filePath.toLowerCase();
return lowerPath.includes('dockerfile') ||
lowerPath.endsWith('docker-compose.yml') ||
lowerPath.endsWith('docker-compose.yaml') ||
lowerPath.endsWith('.dockerfile');
}
analyzeDockerfileContext(content) {
const runMatches = content.match(/^RUN\s+/gm) || [];
const copyMatches = content.match(/^COPY\s+/gm) || [];
return {
hasUserDirective: /^USER\s+/m.test(content),
hasFromDirective: /^FROM\s+/m.test(content),
hasWorkdirDirective: /^WORKDIR\s+/m.test(content),
baseImage: this.extractBaseImage(content),
isMultiStage: (content.match(/^FROM\s+/gm) || []).length > 1,
stageCount: (content.match(/^FROM\s+/gm) || []).length,
hasSecurityLabels: /LABEL\s+.*security/m.test(content),
exposedPorts: this.extractExposedPorts(content),
hasHealthcheck: /^HEALTHCHECK/m.test(content),
hasNonRootUser: /^USER\s+(?!root\b)/m.test(content),
hasDockerignore: this.checkDockerignoreExists(),
hasMaintainerLabel: /LABEL\s+.*maintainer/m.test(content),
runCommandCount: runMatches.length,
copyCommandCount: copyMatches.length,
userDirectiveOrder: this.extractUserDirectiveOrder(content),
hasExcessiveLayers: runMatches.length > 10 || copyMatches.length > 5,
hasPublicPorts: this.hasPublicPorts(content)
};
}
extractBaseImage(content) {
const fromMatch = content.match(/^FROM\s+([^\s]+)/m);
return fromMatch?.[1] ?? 'unknown';
}
extractExposedPorts(content) {
const exposeMatches = content.match(/^EXPOSE\s+([^\n]+)/gm);
if (!exposeMatches)
return [];
return exposeMatches.map(match => {
const portMatch = match.match(/^EXPOSE\s+(.+)/);
return portMatch?.[1]?.trim() ?? '';
}).filter(port => port.length > 0);
}
checkDockerignoreExists() {
return false;
}
extractUserDirectiveOrder(content) {
const lines = content.split('\n');
const userLines = [];
lines.forEach((line, index) => {
if (/^USER\s+/m.test(line)) {
userLines.push(index + 1);
}
});
return userLines;
}
hasPublicPorts(content) {
const publicPorts = ['80', '443', '8080', '3000', '5000'];
const exposedPorts = this.extractExposedPorts(content);
return exposedPorts.some(port => publicPorts.some(publicPort => port.includes(publicPort)));
}
shouldSkipMatch(type, context) {
if (type === 'Root User Execution' && context.hasNonRootUser) {
return true;
}
if (type === 'Latest Tag Usage' && context.isMultiStage) {
return false;
}
return false;
}
determineSeverity(baseSeverity, context, type) {
if (type === 'Root User Execution' && !context.hasNonRootUser) {
return 'critical';
}
if (type === 'Latest Tag Usage' && context.baseImage.includes('latest')) {
return 'high';
}
if (type === 'Sensitive Environment Variables') {
return 'critical';
}
if (type === 'Port Exposure' && context.hasPublicPorts) {
return 'high';
}
if (type === 'Missing Image Digest' && !context.isMultiStage) {
return 'high';
}
if (type === 'Insecure Download') {
return 'high';
}
return baseSeverity;
}
enhanceSuggestion(baseSuggestion, context, type) {
let suggestion = baseSuggestion;
if (type === 'Root User Execution') {
suggestion += '\n\n**Complete fix example:**\n```dockerfile\nRUN adduser --disabled-password --gecos "" appuser \\\n && chown -R appuser:appuser /app \\\n && USER appuser\n```';
}
if (type === 'Latest Tag Usage') {
suggestion += `\n\n**Current base image:** ${context.baseImage}`;
suggestion += '\n**Recommended:** Use specific version tags for reproducible builds';
}
if (type === 'Port Exposure' && context.exposedPorts.length > 0) {
suggestion += `\n\n**Currently exposed ports:** ${context.exposedPorts.join(', ')}`;
if (context.hasPublicPorts) {
suggestion += '\n**⚠️ Critical:** Public ports (80, 443, 8080) exposed - document services behind ports';
}
suggestion += '\n**Consider:** Only expose necessary ports and use non-standard ports for internal services';
}
if (type === 'Missing Image Digest') {
suggestion += '\n\n**Security benefit:** Pinned digests prevent supply chain attacks and ensure immutable images';
suggestion += '\n**Example:** FROM node:18.17.0-alpine@sha256:abc123def456...';
}
if (type === 'Package Installation') {
suggestion += '\n\n**Additional security:** Pin package versions to prevent supply chain attacks';
suggestion += '\n**Example:** RUN apt-get update && apt-get install -y package=1.2.3 && rm -rf /var/lib/apt/lists/*';
}
if (type === 'ADD Directive Usage') {
suggestion += '\n\n**Security risk:** ADD can extract archives and download from URLs';
suggestion += '\n**Solution:** Use COPY for local files, download and verify external resources separately';
suggestion += '\n**Also add:** .dockerignore file to prevent sensitive file leaks';
}
if (type === 'Unchained RUN Commands') {
suggestion += '\n\n**Security risk:** Unchained commands can hide failures and create inconsistent states';
suggestion += '\n**Best practice:** Always chain with && to ensure commands fail fast';
}
if (type === 'Large Base Image') {
suggestion += '\n\n**Security benefit:** Minimal images reduce attack surface and image size';
suggestion += '\n**Alternatives:** alpine, distroless, or scratch for production workloads';
}
return suggestion;
}
checkMissingPractices(context) {
const issues = [];
if (!context.hasUserDirective && !context.hasNonRootUser) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing non-root user: Container runs as root by default', 'Add a non-root user: RUN adduser --disabled-password --gecos "" appuser && USER appuser', 'high'));
}
if (!context.hasWorkdirDirective) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing WORKDIR: Defaults to / which can cause permission issues', 'Add WORKDIR: WORKDIR /app to set a specific working directory', 'medium'));
}
if (!context.hasSecurityLabels) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing security labels: No security metadata provided', 'Add security labels: LABEL security.scan="enabled" security.level="high"', 'medium'));
}
if (!context.hasMaintainerLabel) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing maintainer label: No responsible contact information', 'Add maintainer: LABEL maintainer="your-email@example.com"', 'low'));
}
if (!context.hasHealthcheck) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing healthcheck: No container health monitoring', 'Add healthcheck: HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 CMD curl -f http://localhost:3000/health || exit 1', 'medium'));
}
if (context.hasExcessiveLayers) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'RUN/COPY ...', 'Excessive layers: Too many RUN/COPY commands create large, insecure images', `Combine commands: ${context.runCommandCount} RUN commands and ${context.copyCommandCount} COPY commands detected. Use multi-line RUN with && chaining`, 'medium'));
}
if (context.exposedPorts.length > 3) {
const severity = context.hasPublicPorts ? 'high' : 'medium';
issues.push(this.createIssue('Dockerfile', 1, 1, 'EXPOSE ...', 'Excessive port exposure: Too many ports exposed', `Consider reducing exposed ports. Currently exposing: ${context.exposedPorts.join(', ')}${context.hasPublicPorts ? ' (includes public ports)' : ''}`, severity));
}
if (!context.hasDockerignore) {
issues.push(this.createIssue('Dockerfile', 1, 1, 'FROM ...', 'Missing .dockerignore: Risk of including sensitive files in build context', 'Create .dockerignore file to exclude sensitive files, node_modules, .git, etc.', 'medium'));
}
return issues;
}
}
exports.DockerfileSecurityRule = DockerfileSecurityRule;
//# sourceMappingURL=dockerfile-security.js.map