UNPKG

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
"use strict"; 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;