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.

476 lines (475 loc) • 22 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ContainerImageScanner = void 0; const tslib_1 = require("tslib"); const client_sts_1 = require("@aws-sdk/client-sts"); const client_organizations_1 = require("@aws-sdk/client-organizations"); const fs = tslib_1.__importStar(require("fs/promises")); const chalk_1 = tslib_1.__importDefault(require("chalk")); const ora_1 = tslib_1.__importDefault(require("ora")); const cli_table3_1 = tslib_1.__importDefault(require("cli-table3")); class ContainerImageScanner { constructor() { Object.defineProperty(this, "results", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "startTime", { enumerable: true, configurable: true, writable: true, value: Date.now() }); Object.defineProperty(this, "bitnamiImages", { enumerable: true, configurable: true, writable: true, value: new Set([ 'nginx', 'apache', 'haproxy', 'envoy', 'contour', 'configurable-http-proxy', 'nginx-ingress-controller', 'kong-ingress-controller', 'mysql', 'postgresql', 'mongodb', 'redis', 'mariadb', 'cassandra', 'elasticsearch', 'neo4j', 'clickhouse', 'influxdb', 'scylladb', 'valkey', 'redis-cluster', 'redis-sentinel', 'mongodb-sharded', 'mariadb-galera', 'postgresql-repmgr', 'etcd', 'consul', 'zookeeper', 'solr', 'memcached', 'pgbouncer', 'pgpool', 'percona-mysql', 'kafka', 'rabbitmq', 'nats', 'rabbitmq-cluster-operator', 'schema-registry', 'rmq-default-credential-updater', 'rmq-messaging-topology-operator', 'node', 'python', 'php-fpm', 'ruby', 'java', 'dotnet', 'golang', 'express', 'rails', 'django', 'flask', 'spring-boot', 'quarkus', 'micronaut', 'dotnet-sdk', 'rust', 'wordpress', 'drupal', 'moodle', 'discourse', 'ghost', 'mastodon', 'wordpress-nginx', 'mediawiki', 'phpmyadmin', 'adminer', 'joomla', 'typo3', 'concrete5', 'modx', 'jenkins', 'gitlab-runner', 'argo-cd', 'argo-workflow-controller', 'argo-workflow-cli', 'argo-workflow-exec', 'gitea', 'jenkins-agent', 'concourse', 'harbor-core', 'harbor-portal', 'harbor-registry', 'harbor-registryctl', 'harbor-jobservice', 'harbor-adapter-trivy', 'harbor-exporter', 'git', 'gitlab-runner-helper', 'prometheus', 'grafana', 'grafana-operator', 'jaeger', 'fluent-bit', 'fluentd', 'logstash', 'elasticsearch-exporter', 'node-exporter', 'blackbox-exporter', 'statsd-exporter', 'grafana-image-renderer', 'grafana-loki', 'grafana-tempo', 'grafana-tempo-query', 'grafana-mimir', 'grafana-pyroscope', 'grafana-tempo-vulture', 'grafana-alloy', 'prometheus-operator', 'prometheus-rsocket-proxy', 'thanos', 'hubble-relay', 'hubble-ui', 'hubble-ui-backend', 'telegraf', 'kube-state-metrics', 'metrics-server' ]) }); this.initializeResults(); } isBitnamiImage(imageName) { if (!imageName || typeof imageName !== 'string') { return false; } const normalizedImage = imageName.toLowerCase(); if (normalizedImage.includes('/bitnami/') || normalizedImage.startsWith('bitnami/')) { return true; } if (normalizedImage.includes('/bitnami-labs/')) { return true; } return false; } assessRiskLevel(imageName) { if (!imageName) return 'LOW'; if (imageName.includes(':latest')) { return 'CRITICAL'; } const highRiskServices = ['mysql', 'postgresql', 'elasticsearch', 'mongodb', 'kafka']; if (highRiskServices.some(service => imageName.includes(service))) { return 'HIGH'; } const mediumRiskServices = ['nginx', 'apache', 'redis', 'rabbitmq']; if (mediumRiskServices.some(service => imageName.includes(service))) { return 'MEDIUM'; } return 'LOW'; } getAwsAlternative(imageName) { const serviceMap = { 'mysql': 'Amazon RDS for MySQL', 'postgresql': 'Amazon Aurora PostgreSQL', 'mongodb': 'Amazon DocumentDB', 'redis': 'Amazon ElastiCache for Redis', 'elasticsearch': 'Amazon OpenSearch Service', 'influxdb': 'Amazon Timestream', 'grafana': 'Amazon Managed Grafana', 'prometheus': 'Amazon Managed Service for Prometheus', 'kafka': 'Amazon MSK', 'rabbitmq': 'Amazon MQ', 'nginx': 'AWS Load Balancer Controller + ALB', 'apache': 'AWS Load Balancer Controller + ALB', 'haproxy': 'AWS Load Balancer Controller + NLB', 'mariadb': 'Amazon RDS for MariaDB', 'nats': 'Amazon MQ', }; for (const [service, alternative] of Object.entries(serviceMap)) { if (imageName.toLowerCase().includes(service)) { return alternative; } } return undefined; } searchImages(searchText, fields = ['image', 'container', 'namespace']) { const searchLower = searchText.toLowerCase(); return this.results.images.filter(image => { return fields.some(field => { const value = image[field]; return value && value.toString().toLowerCase().includes(searchLower); }); }); } filterImages(filters) { return this.results.images.filter(image => { return filters.every(filter => { const value = image[filter.field]; switch (filter.operator) { case 'equals': return value === filter.value; case 'contains': return value && value.toString().toLowerCase().includes(filter.value.toLowerCase()); case 'startsWith': return value && value.toString().toLowerCase().startsWith(filter.value.toLowerCase()); case 'in': return Array.isArray(filter.value) && filter.value.includes(value); default: return true; } }); }); } getClusterSummary() { const clusterMap = new Map(); this.results.images.forEach(image => { const key = `${image.account}-${image.region}-${image.cluster}`; if (!clusterMap.has(key)) { clusterMap.set(key, { account: image.account, accountName: image.accountName, region: image.region, cluster: image.cluster, totalImages: 0, bitnamiImages: 0, criticalImages: 0, namespaces: new Set() }); } const cluster = clusterMap.get(key); cluster.totalImages++; if (image.image.includes('bitnami/')) cluster.bitnamiImages++; if (image.riskLevel === 'CRITICAL') cluster.criticalImages++; cluster.namespaces.add(image.namespace); }); return Array.from(clusterMap.values()).map(cluster => ({ ...cluster, namespacesCount: cluster.namespaces.size })); } async startInteractiveMode() { const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: chalk_1.default.cyan('cis> ') }); console.log(chalk_1.default.blue.bold('\nšŸ” Interactive Query Mode')); console.log(chalk_1.default.gray('Commands: search <text>, filter <field> <value>, clusters, exit\n')); rl.prompt(); rl.on('line', (input) => { const [cmd, ...args] = input.trim().split(' '); switch (cmd) { case 'search': const searchResults = this.searchImages(args.join(' '), ['image', 'container', 'namespace']); this.displayResults(searchResults.slice(0, 10)); break; case 'filter': if (args.length >= 2) { const field = args[0] || ''; const value = args.slice(1).join(' ') || ''; const filterResults = this.filterImages([{ field: field, operator: 'contains', value: value }]); this.displayResults(filterResults.slice(0, 10)); } break; case 'clusters': this.displayClusterSummary(); break; case 'exit': rl.close(); return; default: console.log(chalk_1.default.yellow('Available commands: search, filter, clusters, exit')); } rl.prompt(); }); } displayResults(images) { if (images) { const table = new cli_table3_1.default({ head: [chalk_1.default.cyan('Cluster'), chalk_1.default.cyan('Namespace'), chalk_1.default.cyan('Image'), chalk_1.default.cyan('Risk')] }); images.forEach(image => { const riskColor = image.riskLevel === 'CRITICAL' ? chalk_1.default.red : image.riskLevel === 'HIGH' ? chalk_1.default.yellow : chalk_1.default.green; table.push([image.cluster, image.namespace, image.image, riskColor(image.riskLevel)]); }); console.log(table.toString()); } else { console.log(chalk_1.default.bgBlue.white.bold('\n šŸ” CONTAINER IMAGE ANALYSIS RESULTS \n')); const summaryTable = new cli_table3_1.default({ head: [chalk_1.default.cyan('Scan Summary'), chalk_1.default.cyan('Current State'), chalk_1.default.cyan('Action Required')], colWidths: [25, 20, 35] }); const bitnamiCount = this.results.images.filter(img => img.image.includes('bitnami')).length; summaryTable.push(['Total Images Scanned', this.results.summary.totalImages.toString(), 'Complete dependency analysis'], ['Bitnami Images Found', chalk_1.default.red.bold(bitnamiCount.toString()), bitnamiCount > 0 ? 'Review migration options' : 'No action needed'], ['Critical Risk (latest)', chalk_1.default.red.bold(this.results.summary.criticalRisk.toString()), 'Replace latest tags'], ['High Risk Images', chalk_1.default.yellow.bold(this.results.summary.highRisk.toString()), 'Version pin recommended']); console.log(summaryTable.toString()); console.log(chalk_1.default.bold.cyan('\nšŸ” Scan Scope & Coverage\n')); console.log(chalk_1.default.cyan(`āœ… Scanned ${this.results.metadata.totalAccounts} AWS accounts`)); console.log(chalk_1.default.cyan(`āœ… Analyzed ${this.results.clusters.length} EKS clusters`)); console.log(chalk_1.default.cyan(`āœ… Checked against ${this.bitnamiImages.size} known Bitnami images`)); console.log(chalk_1.default.cyan(`āœ… Found ${this.results.summary.totalImages} total container images`)); if (bitnamiCount > 0) { console.log(chalk_1.default.yellow(`āš ļø FOUND ${bitnamiCount} BITNAMI IMAGES requiring review`)); } else { console.log(chalk_1.default.green(`āœ… NO BITNAMI IMAGES detected`)); } } } displayClusterSummary() { const clusters = this.getClusterSummary(); const table = new cli_table3_1.default({ head: [chalk_1.default.cyan('Cluster'), chalk_1.default.cyan('Images'), chalk_1.default.cyan('Bitnami'), chalk_1.default.cyan('Critical')] }); clusters.forEach(cluster => { table.push([ cluster.cluster, cluster.totalImages.toString(), chalk_1.default.yellow(cluster.bitnamiImages.toString()), cluster.criticalImages > 0 ? chalk_1.default.red(cluster.criticalImages.toString()) : '0' ]); }); console.log(table.toString()); } processScanResults(scanData) { const summary = { totalImages: 0, bitnamiImages: 0, criticalRisk: 0, highRisk: 0, mediumRisk: 0, lowRisk: 0 }; const processedClusters = scanData.clusters.map((cluster) => { const clusterSummary = this.processCluster(cluster); if (cluster.images) { summary.totalImages += cluster.images.length; cluster.images.forEach((image) => { if (this.isBitnamiImage(image)) { summary.bitnamiImages++; const risk = this.assessRiskLevel(image); switch (risk) { case 'CRITICAL': summary.criticalRisk++; break; case 'HIGH': summary.highRisk++; break; case 'MEDIUM': summary.mediumRisk++; break; case 'LOW': summary.lowRisk++; break; } } }); } return clusterSummary; }); return { summary, clusters: processedClusters }; } processCluster(cluster) { let bitnamiCount = 0; let highestRisk = 'LOW'; if (cluster.images) { cluster.images.forEach((image) => { if (this.isBitnamiImage(image)) { bitnamiCount++; const risk = this.assessRiskLevel(image); if (this.getRiskPriority(risk) > this.getRiskPriority(highestRisk)) { highestRisk = risk; } } }); } return { ...cluster, bitnamiCount, riskLevel: highestRisk }; } getRiskPriority(risk) { const priorities = { 'LOW': 1, 'MEDIUM': 2, 'HIGH': 3, 'CRITICAL': 4 }; return priorities[risk] || 0; } validateAccountId(accountId) { return /^\d{12}$/.test(accountId); } validateRegion(region) { const validRegions = [ 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-central-1', 'eu-north-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', 'ap-south-1', 'ca-central-1', 'sa-east-1', 'af-south-1', 'me-south-1' ]; return validRegions.includes(region); } initializeResults() { const deadline = new Date('2025-08-28'); const now = new Date(); const daysUntilDeadline = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); this.results = { metadata: { scanId: `scan-${Date.now()}`, timestamp: now.toISOString(), version: '2.0.0', totalAccounts: 0, totalClusters: 0, daysUntilDeadline, broadcomDeadline: '2025-08-28', scanDuration: 0 }, summary: { totalImages: 0, criticalRisk: 0, highRisk: 0, mediumRisk: 0, lowRisk: 0, categorizedImages: {}, topRiskClusters: [] }, images: [], clusters: [], errors: [] }; } async performScan(options) { this.startTime = Date.now(); const regions = options.regions.split(',').map(r => r.trim()); let accounts = []; try { if (options.orgScan) { console.log(chalk_1.default.yellow('šŸ¢ Discovering AWS Organization accounts...\n')); accounts = await this.discoverOrganizationAccounts(); } else if (options.accounts) { accounts = options.accounts.split(',').map(id => ({ id: id.trim(), name: `Account-${id.trim()}` })); } else { const stsClient = new client_sts_1.STSClient({}); const identity = await stsClient.send(new client_sts_1.GetCallerIdentityCommand({})); accounts = [{ id: identity.Account, name: 'Current-Account' }]; } console.log(chalk_1.default.blue(`šŸŽÆ Scanning ${accounts.length} accounts across ${regions.length} regions`)); console.log(chalk_1.default.blue(`šŸ” Looking for ${this.bitnamiImages.size} known Bitnami container images\n`)); const chunkSize = 5; for (let i = 0; i < accounts.length; i += chunkSize) { const chunk = accounts.slice(i, i + chunkSize); const promises = chunk.map(account => this.scanAccount(account.id, account.name, regions)); await Promise.allSettled(promises); } this.calculateSummary(); this.displayResults(); if (options.output) { await this.saveResults(options.output); } const duration = Math.round((Date.now() - this.startTime) / 1000); this.results.metadata.scanDuration = duration; console.log(chalk_1.default.green(`\nāœ… Scan completed in ${duration}s`)); } catch (error) { console.error(chalk_1.default.red(`āŒ Fatal scan error: ${error.message}`)); throw error; } } async discoverOrganizationAccounts() { const spinner = (0, ora_1.default)('Discovering AWS Organization accounts...').start(); try { const orgClient = new client_organizations_1.OrganizationsClient({ region: 'us-east-1' }); const response = await orgClient.send(new client_organizations_1.ListAccountsCommand({})); const accounts = response.Accounts?.map(account => ({ id: account.Id, name: account.Name || `Account-${account.Id}` })) || []; spinner.succeed(`Discovered ${accounts.length} accounts`); return accounts; } catch (error) { spinner.fail(`Failed to discover accounts: ${error.message}`); this.results.errors.push({ account: 'organization', error: `Organization discovery failed: ${error.message}`, timestamp: new Date().toISOString(), errorType: 'AUTH' }); throw error; } } calculateSummary() { this.results.metadata.totalAccounts = new Set(this.results.images.map(img => img.account)).size; this.results.metadata.totalClusters = this.results.clusters.length; this.results.summary.totalImages = this.results.images.length; this.results.summary.criticalRisk = this.results.images.filter(img => img.riskLevel === 'CRITICAL').length; this.results.summary.highRisk = this.results.images.filter(img => img.riskLevel === 'HIGH').length; this.results.summary.mediumRisk = this.results.images.filter(img => img.riskLevel === 'MEDIUM').length; this.results.summary.lowRisk = this.results.images.filter(img => img.riskLevel === 'LOW').length; for (const image of this.results.images) { this.results.summary.categorizedImages[image.category] = (this.results.summary.categorizedImages[image.category] || 0) + 1; } const clusterRisks = this.results.clusters .map(cluster => ({ cluster: cluster.name, account: cluster.account, region: cluster.region, bitnamiCount: cluster.bitnamiImageCount, criticalCount: this.results.images.filter(img => img.cluster === cluster.name && img.account === cluster.account && img.riskLevel === 'CRITICAL').length })) .filter(cr => cr.bitnamiCount > 0) .sort((a, b) => b.criticalCount - a.criticalCount || b.bitnamiCount - a.bitnamiCount) .slice(0, 5); this.results.summary.topRiskClusters = clusterRisks; } async saveResults(outputFile) { try { await fs.writeFile(outputFile, JSON.stringify(this.results, null, 2)); console.log(chalk_1.default.green(`\nāœ… Results saved to ${outputFile}`)); } catch (error) { console.error(chalk_1.default.red(`āŒ Failed to save results: ${error.message}`)); } } async scanAccount(accountId, accountName, regions) { console.log(`Scanning account ${accountName} (${accountId}) in regions: ${regions.join(', ')}`); } async generateRoleSetupInstructions(_options) { console.log('Generating role setup instructions...'); } } exports.ContainerImageScanner = ContainerImageScanner;