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
JavaScript
"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`;
}