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
JavaScript
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;
;