UNPKG

carrot-scan

Version:

Command-line tool for detecting vulnerabilities in files and directories.

4 lines (1 loc) 6.79 kB
import { Plugin } from '../src/plugin-interface.js';import fs from 'fs/promises';import path from 'path';export class DockerfilePlugin extends Plugin { static applies(file) { return path.basename(file).toLowerCase() === 'dockerfile'; } async run(file, context) { const issues = []; const content = await fs.readFile(file, 'utf-8'); const lines = content.split('\n'); // Check that FROM is the first instruction if (!lines[0].trim().toUpperCase().startsWith('FROM')) { issues.push({ file, line: 1, message: 'Dockerfile should start with a FROM instruction.', }); } // Check that a user is set and it is not root let userSet = false; for (const line of lines) { if (line.trim().toUpperCase().startsWith('USER')) { userSet = true; if (line.trim().split(/\s+/)[1] === 'root') { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Avoid running containers as root user.', }); } } } if (!userSet) { issues.push({ file, line: 1, message: 'A user should be set in the Dockerfile.', }); } // Check that no ADD commands are used for (const line of lines) { if (line.trim().toUpperCase().startsWith('ADD')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use COPY instead of ADD.', }); } } // Check that no secrets are leaked for (const line of lines) { if (line.match(/password|secret|token/i)) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Potential secret leak.', }); } } // Check that a HEALTHCHECK is defined let healthcheckSet = false; for (const line of lines) { if (line.trim().toUpperCase().startsWith('HEALTHCHECK')) { healthcheckSet = true; } } if (!healthcheckSet) { issues.push({ file, line: 1, message: 'A HEALTHCHECK should be defined.', }); } // Check that a .dockerignore file is present try { await fs.access(path.join(path.dirname(file), '.dockerignore')); } catch (err) { issues.push({ file, line: 1, message: 'A .dockerignore file should be present.', }); } // Check that WORKDIR is an absolute path for (const line of lines) { if (line.trim().toUpperCase().startsWith('WORKDIR')) { if (!path.isAbsolute(line.trim().split(/\s+/)[1])) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'WORKDIR should be an absolute path.', }); } } } // Check that no sudo or su commands are used for (const line of lines) { if (line.match(/sudo|su/i)) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Avoid using sudo or su.', }); } } // Check that RUN commands are not split into multiple layers for (let i = 0; i < lines.length - 1; i++) { if (lines[i].trim().toUpperCase().startsWith('RUN') && lines[i + 1].trim().toUpperCase().startsWith('RUN')) { issues.push({ file, line: i + 2, message: 'Combine RUN commands to reduce layer count.', }); } } // Check that EXPOSE only contains numbers for (const line of lines) { if (line.trim().toUpperCase().startsWith('EXPOSE')) { if (!/^[0-9]+$/.test(line.trim().split(/\s+/)[1])) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'EXPOSE should only contain numbers.', }); } } } // Check that no latest tag is used in FROM for (const line of lines) { if (line.trim().toUpperCase().startsWith('FROM')) { if (line.trim().split(/\s+/)[1].endsWith(':latest')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Avoid using the latest tag in FROM.', }); } } } // Check that LABELs are used let labelSet = false; for (const line of lines) { if (line.trim().toUpperCase().startsWith('LABEL')) { labelSet = true; } } if (!labelSet) { issues.push({ file, line: 1, message: 'LABELs should be used to organize images.', }); } // Check that CMD and ENTRYPOINT are in exec form for (const line of lines) { if (line.trim().toUpperCase().startsWith('CMD') || line.trim().toUpperCase().startsWith('ENTRYPOINT')) { if (!line.includes('[')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use exec form for CMD and ENTRYPOINT.', }); } } } // Check that apt-get is used with --no-install-recommends and that the cache is cleaned up for (const line of lines) { if (line.includes('apt-get install')) { if (!line.includes('--no-install-recommends')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use --no-install-recommends with apt-get install.', }); } if (!line.includes('rm -rf /var/lib/apt/lists/*')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Clean up apt-get cache.', }); } } } // Check that npm is used with --no-update-notifier --no-fund for (const line of lines) { if (line.includes('npm install')) { if (!line.includes('--no-update-notifier --no-fund')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use --no-update-notifier --no-fund with npm install.', }); } } } // Check that pip is used with --no-cache-dir for (const line of lines) { if (line.includes('pip install')) { if (!line.includes('--no-cache-dir')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use --no-cache-dir with pip install.', }); } } } // Check that curl is used with -fsSL for (const line of lines) { if (line.includes('curl')) { if (!line.includes('-fsSL')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use -fsSL with curl.', }); } } } // Check that wget is used with -q for (const line of lines) { if (line.includes('wget')) { if (!line.includes('-q')) { issues.push({ file, line: lines.indexOf(line) + 1, message: 'Use -q with wget.', }); } } } return issues; }}