UNPKG

aws-container-image-scanner

Version:

AWS Container Image Scanner - Enterprise tool for scanning EKS clusters, analyzing Bitnami container dependencies, and generating migration guidance for AWS ECR alternatives with security best practices.

411 lines (402 loc) • 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.startUIServer = startUIServer; const tslib_1 = require("tslib"); const express_1 = tslib_1.__importDefault(require("express")); const cors_1 = tslib_1.__importDefault(require("cors")); const helmet_1 = tslib_1.__importDefault(require("helmet")); const express_rate_limit_1 = tslib_1.__importDefault(require("express-rate-limit")); const express_basic_auth_1 = tslib_1.__importDefault(require("express-basic-auth")); const compression_1 = tslib_1.__importDefault(require("compression")); const path_1 = tslib_1.__importDefault(require("path")); const chalk_1 = tslib_1.__importDefault(require("chalk")); const open_1 = tslib_1.__importDefault(require("open")); const uuid_1 = require("uuid"); async function startUIServer(port = 3000, options = {}) { const app = (0, express_1.default)(); const jobs = new Map(); app.use((0, helmet_1.default)({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'"], fontSrc: ["'self'"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"] } }, crossOriginEmbedderPolicy: false, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } })); const limiter = (0, express_rate_limit_1.default)({ windowMs: 15 * 60 * 1000, max: 100, message: { error: 'Too many requests from this IP, please try again later.' }, standardHeaders: true, legacyHeaders: false, }); app.use('/api/', limiter); const scanLimiter = (0, express_rate_limit_1.default)({ windowMs: 60 * 60 * 1000, max: 10, message: { error: 'Scan rate limit exceeded. Maximum 10 scans per hour.' } }); app.use('/api/scan', scanLimiter); app.use((0, compression_1.default)()); app.use((0, cors_1.default)({ origin: process.env.NODE_ENV === 'production' ? ['https://localhost:3000', 'https://127.0.0.1:3000'] : ['http://localhost:3000', 'http://127.0.0.1:3000'], credentials: true, methods: ['GET', 'POST', 'DELETE'], allowedHeaders: ['Content-Type', 'Authorization'] })); app.use(express_1.default.json({ limit: '10mb' })); app.use(express_1.default.urlencoded({ extended: true, limit: '10mb' })); if (options.auth) { app.use((0, express_basic_auth_1.default)({ users: { [options.auth.username]: options.auth.password }, challenge: true, realm: 'Container Image Scanner' })); } app.use((req, _res, next) => { const timestamp = new Date().toISOString(); console.log(`${timestamp} ${req.method} ${req.path} - ${req.ip}`); next(); }); app.use(express_1.default.static(path_1.default.join(__dirname, '../ui/build'), { maxAge: '1d', etag: true, lastModified: true })); app.get('/api/health', (_req, res) => { return res.json({ status: 'healthy', version: '2.5.0', tool: 'Container Image Scanner', timestamp: new Date().toISOString(), uptime: process.uptime() }); }); app.get('/api/system-info', (_req, res) => { return res.json({ nodeVersion: process.version, platform: process.platform, timestamp: new Date().toISOString(), memory: { used: Math.round(process.memoryUsage().heapUsed / 1024 / 1024), total: Math.round(process.memoryUsage().heapTotal / 1024 / 1024) } }); }); const validateScanInput = (req, res, next) => { const { accounts, regions, roleArn } = req.body; if (!regions || !Array.isArray(regions) || regions.length === 0) { return res.status(400).json({ error: 'At least one valid AWS region is required', code: 'INVALID_REGIONS' }); } const validRegionPattern = /^[a-z0-9-]+$/; for (const region of regions) { if (!validRegionPattern.test(region)) { return res.status(400).json({ error: `Invalid region format: ${region}`, code: 'INVALID_REGION_FORMAT' }); } } if (accounts && Array.isArray(accounts)) { const validAccountPattern = /^\d{12}$/; for (const account of accounts) { if (!validAccountPattern.test(account)) { return res.status(400).json({ error: `Invalid AWS account ID format: ${account}`, code: 'INVALID_ACCOUNT_ID' }); } } } if (roleArn && typeof roleArn === 'string') { const validRoleArnPattern = /^arn:aws:iam::\d{12}:role\/[\w+=,.@-]+$/; if (!validRoleArnPattern.test(roleArn)) { return res.status(400).json({ error: 'Invalid IAM role ARN format', code: 'INVALID_ROLE_ARN' }); } } return next(); }; app.post('/api/scan', validateScanInput, async (req, res) => { try { const { accounts, regions, orgScan, roleArn } = req.body; const scanId = (0, uuid_1.v4)(); const job = { id: scanId, status: 'queued', progress: 0, startTime: new Date(), options: { accounts: accounts || [], regions, orgScan: Boolean(orgScan), roleArn: roleArn || null }, logs: [], userId: req.ip }; jobs.set(scanId, job); console.log(`Scan ${scanId} initiated by ${req.ip}`); return res.json({ scanId, status: 'started', message: 'Scan started successfully', timestamp: new Date().toISOString() }); } catch (error) { console.error('Scan initiation error:', error.message); return res.status(500).json({ error: 'Internal server error', code: 'SCAN_INIT_ERROR' }); } }); app.get('/api/scan/:id', (req, res) => { const scanId = req.params.id; const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; if (!uuidPattern.test(scanId)) { return res.status(400).json({ error: 'Invalid scan ID format', code: 'INVALID_SCAN_ID' }); } const job = jobs.get(scanId); if (!job) { return res.status(404).json({ error: 'Scan job not found', code: 'SCAN_NOT_FOUND' }); } if (job.userId && job.userId !== req.ip) { return res.status(403).json({ error: 'Access denied', code: 'ACCESS_DENIED' }); } return res.json({ id: job.id, status: job.status, progress: job.progress, startTime: job.startTime, endTime: job.endTime, error: job.error, results: job.results ? { summary: job.results.summary, imageCount: job.results.images?.length || 0, } : null }); }); app.get('/api/scan/:id/logs', (req, res) => { const scanId = req.params.id; const job = jobs.get(scanId); if (!job) { return res.status(404).json({ error: 'Scan job not found', code: 'SCAN_NOT_FOUND' }); } if (job.userId && job.userId !== req.ip) { return res.status(403).json({ error: 'Access denied', code: 'ACCESS_DENIED' }); } return res.json({ logs: job.logs, count: job.logs.length }); }); app.delete('/api/scan/:id', (req, res) => { const scanId = req.params.id; const job = jobs.get(scanId); if (!job) { return res.status(404).json({ error: 'Scan job not found', code: 'SCAN_NOT_FOUND' }); } if (job.userId && job.userId !== req.ip) { return res.status(403).json({ error: 'Access denied', code: 'ACCESS_DENIED' }); } if (job.status === 'running' && job.process) { job.process.kill('SIGTERM'); } jobs.delete(scanId); console.log(`Scan ${scanId} cancelled by ${req.ip}`); return res.json({ message: 'Scan job cancelled', timestamp: new Date().toISOString() }); }); app.get('/api/scans', (req, res) => { const userJobs = Array.from(jobs.values()).filter(job => !job.userId || job.userId === req.ip); return res.json({ jobs: userJobs.map(job => ({ id: job.id, status: job.status, startTime: job.startTime, endTime: job.endTime, progress: job.progress })), total: userJobs.length }); }); app.post('/api/generate-migration', async (req, res) => { try { const { scanId, scriptType = 'bash' } = req.body; if (!scanId || typeof scanId !== 'string') { return res.status(400).json({ error: 'Valid scan ID is required', code: 'INVALID_SCAN_ID' }); } if (!['bash', 'powershell'].includes(scriptType)) { return res.status(400).json({ error: 'Script type must be bash or powershell', code: 'INVALID_SCRIPT_TYPE' }); } const job = jobs.get(scanId); if (!job || !job.results) { return res.status(404).json({ error: 'Scan results not found', code: 'RESULTS_NOT_FOUND' }); } if (job.userId && job.userId !== req.ip) { return res.status(403).json({ error: 'Access denied', code: 'ACCESS_DENIED' }); } const script = generateMigrationScript(job.results, scriptType); return res.json({ script, filename: `migration-${scanId}.${scriptType === 'powershell' ? 'ps1' : 'sh'}`, timestamp: new Date().toISOString() }); } catch (error) { console.error('Migration script generation error:', error.message); return res.status(500).json({ error: 'Internal server error', code: 'SCRIPT_GENERATION_ERROR' }); } }); app.use((error, _req, res, next) => { console.error('Unhandled error:', error.message); if (res.headersSent) { return next(error); } return res.status(500).json({ error: 'Internal server error', code: 'INTERNAL_ERROR', timestamp: new Date().toISOString() }); }); app.get('*', (_req, res) => { return res.sendFile(path_1.default.join(__dirname, '../ui/build/index.html')); }); const server = app.listen(port, options.host || 'localhost', () => { console.log(chalk_1.default.bgGreen.black.bold(' šŸ”’ SECURE CONTAINER IMAGE SCANNER UI ')); console.log(chalk_1.default.green(`\nāœ… Server running securely at: ${options.https ? 'https' : 'http'}://localhost:${port}`)); console.log(chalk_1.default.blue(`šŸ›”ļø Security features enabled:`)); console.log(chalk_1.default.blue(` • Helmet security headers`)); console.log(chalk_1.default.blue(` • Rate limiting`)); console.log(chalk_1.default.blue(` • Input validation`)); console.log(chalk_1.default.blue(` • CORS protection`)); console.log(chalk_1.default.blue(` • Compression enabled`)); if (options.auth) { console.log(chalk_1.default.blue(` • Basic authentication`)); } if (options.verbose !== false) { (0, open_1.default)(`${options.https ? 'https' : 'http'}://localhost:${port}`).catch(() => { console.log(chalk_1.default.yellow(`Visit ${options.https ? 'https' : 'http'}://localhost:${port} in your browser`)); }); } }); process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); return new Promise((resolve) => { resolve(); }); } function generateMigrationScript(results, scriptType) { const images = results.images || []; if (scriptType === 'powershell') { return generatePowerShellScript(images); } else { return generateBashScript(images); } } function generateBashScript(images) { return `#!/bin/bash # Container Image Migration Script # Generated: ${new Date().toISOString()} # Images to migrate: ${images.length} set -euo pipefail echo "šŸ”„ Starting migration of ${images.length} container images..." # Add your migration logic here # Example: # for image in "\${images[@]}"; do # echo "Migrating \$image" # # kubectl set image deployment/app container=new-image # done echo "āœ… Migration completed successfully!"`; } function generatePowerShellScript(images) { return `# Container Image Migration Script # Generated: ${new Date().toISOString()} # Images to migrate: ${images.length} $ErrorActionPreference = "Stop" Write-Host "šŸ”„ Starting migration of ${images.length} container images..." -ForegroundColor Green # Add your migration logic here # Example: # foreach ($image in $images) { # Write-Host "Migrating $image" # # kubectl set image deployment/app container=new-image # } Write-Host "āœ… Migration completed successfully!" -ForegroundColor Green`; }