container-image-scanner
Version:
Enterprise Container Image Scanner with AWS Security Best Practices. Scan EKS clusters for Bitnami container image dependencies and generate migration guidance for AWS ECR alternatives.
354 lines (353 loc) ⢠14.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryCommand = void 0;
const tslib_1 = require("tslib");
const chalk_1 = tslib_1.__importDefault(require("chalk"));
const fs = tslib_1.__importStar(require("fs/promises"));
const cli_table3_1 = tslib_1.__importDefault(require("cli-table3"));
class QueryCommand {
constructor() {
Object.defineProperty(this, "scanResults", {
enumerable: true,
configurable: true,
writable: true,
value: null
});
Object.defineProperty(this, "images", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "clusters", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
static async execute(options) {
const queryCmd = new QueryCommand();
await queryCmd.run(options);
}
async run(options) {
try {
await this.loadScanResults(options.input);
if (options.interactive) {
await this.startInteractiveMode();
}
else {
await this.executeQuery(options);
}
}
catch (error) {
throw new Error(`Query execution failed: ${error.message}`);
}
}
async loadScanResults(inputFile) {
try {
const data = await fs.readFile(inputFile, 'utf-8');
this.scanResults = JSON.parse(data);
this.images = this.scanResults?.images || [];
this.clusters = this.scanResults?.clusters || [];
console.log(chalk_1.default.blue(`š Loaded ${this.images.length} images from ${this.clusters.length} clusters`));
}
catch (error) {
throw new Error(`Failed to load scan results: ${error.message}`);
}
}
async executeQuery(options) {
let results = [...this.images];
if (options.search) {
results = this.searchImages(options.search);
console.log(chalk_1.default.blue(`š Search: "${options.search}" found ${results.length} results`));
}
if (options.filter) {
const filters = this.parseFilter(options.filter);
results = this.applyFilters(results, filters);
console.log(chalk_1.default.blue(`š Filter applied: ${results.length} results`));
}
if (options.limit && results.length > options.limit) {
results = results.slice(0, options.limit);
console.log(chalk_1.default.gray(`Showing first ${options.limit} results`));
}
await this.displayResults(results, options.format || 'table');
if (options.output) {
await this.saveResults(results, options.output, options.format || 'json');
}
}
searchImages(searchText) {
const searchLower = searchText.toLowerCase();
return this.images.filter(image => (image.name || image.image).toLowerCase().includes(searchLower) ||
image.tag.toLowerCase().includes(searchLower) ||
image.namespace.toLowerCase().includes(searchLower) ||
image.cluster.toLowerCase().includes(searchLower) ||
(image.containerName || image.container).toLowerCase().includes(searchLower));
}
parseFilter(filterStr) {
const filters = [];
const parts = filterStr.split(' ');
if (parts.length >= 3) {
const field = parts[0] || '';
const operator = (parts[1] || 'contains');
const value = parts.slice(2).join(' ') || '';
filters.push({ field, operator, value });
}
return filters;
}
applyFilters(images, filters) {
return images.filter(image => {
return filters.every(filter => this.matchesFilter(image, filter));
});
}
matchesFilter(image, filter) {
const fieldValue = this.getFieldValue(image, filter.field);
const filterValue = filter.value;
switch (filter.operator) {
case 'equals':
return fieldValue === filterValue;
case 'contains':
return String(fieldValue).toLowerCase().includes(String(filterValue).toLowerCase());
case 'startsWith':
return String(fieldValue).toLowerCase().startsWith(String(filterValue).toLowerCase());
case 'endsWith':
return String(fieldValue).toLowerCase().endsWith(String(filterValue).toLowerCase());
case 'gt':
return Number(fieldValue) > Number(filterValue);
case 'lt':
return Number(fieldValue) < Number(filterValue);
case 'gte':
return Number(fieldValue) >= Number(filterValue);
case 'lte':
return Number(fieldValue) <= Number(filterValue);
default:
return false;
}
}
getFieldValue(image, field) {
const fieldMap = {
'name': image.name || image.image,
'tag': image.tag,
'namespace': image.namespace,
'cluster': image.cluster,
'containerName': image.containerName || image.container,
'riskLevel': image.riskLevel,
'pullPolicy': image.pullPolicy,
'lastUpdated': image.lastUpdated || image.lastScanned,
'size': image.size
};
return fieldMap[field] || '';
}
async displayResults(results, format) {
if (results.length === 0) {
console.log(chalk_1.default.yellow('No results found.'));
return;
}
switch (format) {
case 'table':
this.displayTable(results);
break;
case 'json':
console.log(JSON.stringify(results, null, 2));
break;
case 'csv':
this.displayCSV(results);
break;
default:
this.displayTable(results);
}
}
displayTable(results) {
const table = new cli_table3_1.default({
head: [
chalk_1.default.cyan('Image'),
chalk_1.default.cyan('Tag'),
chalk_1.default.cyan('Cluster'),
chalk_1.default.cyan('Namespace'),
chalk_1.default.cyan('Risk'),
chalk_1.default.cyan('Container')
],
colWidths: [30, 15, 20, 20, 10, 20]
});
results.forEach(image => {
const riskColor = this.getRiskColor(image.riskLevel);
table.push([
image.name || image.image,
image.tag,
image.cluster,
image.namespace,
riskColor(image.riskLevel),
image.containerName || image.container
]);
});
console.log(table.toString());
}
displayCSV(results) {
const headers = ['Image', 'Tag', 'Cluster', 'Namespace', 'Risk', 'Container', 'LastUpdated'];
console.log(headers.join(','));
results.forEach(image => {
const row = [
image.name || image.image,
image.tag,
image.cluster,
image.namespace,
image.riskLevel,
image.containerName || image.container,
image.lastUpdated || image.lastScanned || ''
];
console.log(row.join(','));
});
}
getRiskColor(riskLevel) {
switch (riskLevel) {
case 'CRITICAL':
return chalk_1.default.red.bold;
case 'HIGH':
return chalk_1.default.red;
case 'MEDIUM':
return chalk_1.default.yellow;
case 'LOW':
return chalk_1.default.green;
default:
return chalk_1.default.gray;
}
}
async saveResults(results, outputFile, format) {
let content;
switch (format) {
case 'json':
content = JSON.stringify(results, null, 2);
break;
case 'csv':
const headers = ['Image', 'Tag', 'Cluster', 'Namespace', 'Risk', 'Container', 'LastUpdated'];
const csvRows = [headers.join(',')];
results.forEach(image => {
const row = [
image.name || image.image,
image.tag,
image.cluster,
image.namespace,
image.riskLevel,
image.containerName || image.container,
image.lastUpdated || image.lastScanned || ''
];
csvRows.push(row.join(','));
});
content = csvRows.join('\n');
break;
default:
content = JSON.stringify(results, null, 2);
}
await fs.writeFile(outputFile, content);
console.log(chalk_1.default.green(`ā
Results saved to ${outputFile}`));
}
async startInteractiveMode() {
console.log(chalk_1.default.blue.bold('\nš Interactive Query Mode'));
console.log(chalk_1.default.gray('Enter queries to explore your container data. Type "help" for commands.\n'));
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk_1.default.blue('cis-query> ')
});
rl.prompt();
rl.on('line', async (input) => {
const command = input.trim();
if (command === 'exit' || command === 'quit') {
rl.close();
return;
}
if (command === 'help') {
this.showHelp();
}
else if (command === 'summary') {
this.showSummary();
}
else if (command.startsWith('search ')) {
const searchTerm = command.substring(7);
const results = this.searchImages(searchTerm);
console.log(chalk_1.default.blue(`Found ${results.length} results:`));
this.displayTable(results.slice(0, 10));
}
else if (command.startsWith('filter ')) {
const filterStr = command.substring(7);
const filters = this.parseFilter(filterStr);
const results = this.applyFilters(this.images, filters);
console.log(chalk_1.default.blue(`Filter results: ${results.length} images`));
this.displayTable(results.slice(0, 10));
}
else if (command === 'clusters') {
this.showClusters();
}
else if (command === 'risks') {
this.showRiskSummary();
}
else if (command.startsWith('cluster ')) {
const clusterName = command.substring(8);
const results = this.images.filter(img => img.cluster.includes(clusterName));
console.log(chalk_1.default.blue(`Cluster "${clusterName}": ${results.length} images`));
this.displayTable(results.slice(0, 10));
}
else if (command !== '') {
console.log(chalk_1.default.red('Unknown command. Type "help" for available commands.'));
}
rl.prompt();
});
rl.on('close', () => {
console.log(chalk_1.default.yellow('\nGoodbye!'));
process.exit(0);
});
}
showHelp() {
console.log(chalk_1.default.cyan('\nAvailable Commands:'));
console.log(chalk_1.default.white(' help - Show this help'));
console.log(chalk_1.default.white(' summary - Show scan summary'));
console.log(chalk_1.default.white(' clusters - List all clusters'));
console.log(chalk_1.default.white(' risks - Show risk level summary'));
console.log(chalk_1.default.white(' search <text> - Search images by text'));
console.log(chalk_1.default.white(' filter <field> <op> <val> - Filter images (e.g., "filter riskLevel equals CRITICAL")'));
console.log(chalk_1.default.white(' cluster <name> - Show images in specific cluster'));
console.log(chalk_1.default.white(' exit/quit - Exit interactive mode'));
console.log(chalk_1.default.gray('\nOperators: equals, contains, startsWith, endsWith, gt, lt, gte, lte'));
console.log(chalk_1.default.gray('Fields: name, tag, namespace, cluster, containerName, riskLevel\n'));
}
showSummary() {
const totalImages = this.images.length;
const totalClusters = this.clusters.length;
const riskCounts = this.images.reduce((acc, img) => {
acc[img.riskLevel] = (acc[img.riskLevel] || 0) + 1;
return acc;
}, {});
console.log(chalk_1.default.blue('\nš Scan Summary:'));
console.log(chalk_1.default.white(` Total Images: ${totalImages}`));
console.log(chalk_1.default.white(` Total Clusters: ${totalClusters}`));
console.log(chalk_1.default.white(' Risk Distribution:'));
Object.entries(riskCounts).forEach(([risk, count]) => {
const color = this.getRiskColor(risk);
console.log(chalk_1.default.white(` ${color(risk)}: ${count}`));
});
console.log();
}
showClusters() {
console.log(chalk_1.default.blue('\nšļø Clusters:'));
this.clusters.forEach(cluster => {
const imageCount = this.images.filter(img => img.cluster === cluster.name).length;
console.log(chalk_1.default.white(` ${cluster.name} (${cluster.region}) - ${imageCount} images`));
});
console.log();
}
showRiskSummary() {
const riskCounts = this.images.reduce((acc, img) => {
acc[img.riskLevel] = (acc[img.riskLevel] || 0) + 1;
return acc;
}, {});
console.log(chalk_1.default.blue('\nā ļø Risk Level Summary:'));
Object.entries(riskCounts).forEach(([risk, count]) => {
const color = this.getRiskColor(risk);
const percentage = ((count / this.images.length) * 100).toFixed(1);
console.log(chalk_1.default.white(` ${color(risk)}: ${count} (${percentage}%)`));
});
console.log();
}
}
exports.QueryCommand = QueryCommand;