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.

406 lines (400 loc) 15.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PowerpipeIntegration = void 0; exports.checkPowerpipeInstallation = checkPowerpipeInstallation; exports.installPowerpipe = installPowerpipe; const tslib_1 = require("tslib"); const fs = tslib_1.__importStar(require("fs/promises")); const path = tslib_1.__importStar(require("path")); const chalk_1 = tslib_1.__importDefault(require("chalk")); const child_process_1 = require("child_process"); class PowerpipeIntegration { constructor(config) { Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "scanResults", { enumerable: true, configurable: true, writable: true, value: null }); this.config = { port: 9033, host: 'localhost', ...config }; } async initialize() { try { await fs.mkdir(this.config.workspaceDir, { recursive: true }); await this.copyModFiles(); await this.runPowerpipeCommand(['mod', 'init'], this.config.workspaceDir); console.log(chalk_1.default.green('✅ Powerpipe workspace initialized')); } catch (error) { throw new Error(`Failed to initialize Powerpipe workspace: ${error.message}`); } } async loadScanResults(resultsFile) { try { const data = await fs.readFile(resultsFile, 'utf-8'); this.scanResults = JSON.parse(data); await this.createSQLiteDatabase(); console.log(chalk_1.default.blue(`📊 Loaded ${this.scanResults?.images?.length || 0} images for Powerpipe analysis`)); } catch (error) { throw new Error(`Failed to load scan results: ${error.message}`); } } async startServer() { try { console.log(chalk_1.default.blue(`🚀 Starting Powerpipe server on ${this.config.host}:${this.config.port}`)); const args = [ 'server', '--port', this.config.port.toString(), '--host', this.config.host, '--workspace', this.config.workspaceDir ]; if (this.config.dbPath) { args.push('--database', this.config.dbPath); } const powerpipeProcess = (0, child_process_1.spawn)('powerpipe', args, { cwd: this.config.workspaceDir, stdio: 'inherit' }); powerpipeProcess.on('error', (error) => { console.error(chalk_1.default.red(`❌ Powerpipe server error: ${error.message}`)); }); await new Promise(resolve => setTimeout(resolve, 3000)); console.log(chalk_1.default.green(`✅ Powerpipe server started at http://${this.config.host}:${this.config.port}`)); console.log(chalk_1.default.cyan('Available dashboards:')); console.log(chalk_1.default.cyan(' • Container Overview: /dashboard.container_overview')); console.log(chalk_1.default.cyan(' • Security Analysis: /dashboard.security_analysis')); console.log(chalk_1.default.cyan(' • Migration Planning: /dashboard.migration_planning')); return powerpipeProcess; } catch (error) { throw new Error(`Failed to start Powerpipe server: ${error.message}`); } } async generateCustomDashboard(dashboardName, filters) { if (!this.scanResults) { throw new Error('No scan results loaded. Call loadScanResults() first.'); } const dashboardContent = this.createCustomDashboard(dashboardName, filters); const dashboardPath = path.join(this.config.workspaceDir, `${dashboardName}.pp`); await fs.writeFile(dashboardPath, dashboardContent); console.log(chalk_1.default.green(`✅ Custom dashboard created: ${dashboardPath}`)); } async exportForPowerpipe(format) { if (!this.scanResults) { throw new Error('No scan results loaded'); } const outputPath = path.join(this.config.workspaceDir, `container_data.${format}`); switch (format) { case 'csv': await this.exportToCSV(outputPath); break; case 'json': await this.exportToJSON(outputPath); break; case 'sql': await this.exportToSQL(outputPath); break; } console.log(chalk_1.default.green(`✅ Data exported to ${outputPath}`)); return outputPath; } async runQuery(queryName) { try { const result = await this.runPowerpipeCommand([ 'query', 'run', queryName, '--output', 'json' ], this.config.workspaceDir); return JSON.parse(result); } catch (error) { throw new Error(`Failed to run query ${queryName}: ${error.message}`); } } async generateMigrationReport() { const reportPath = path.join(this.config.workspaceDir, 'migration-report.html'); try { await this.runPowerpipeCommand([ 'dashboard', 'run', 'migration_planning', '--output', 'html', '--export', reportPath ], this.config.workspaceDir); console.log(chalk_1.default.green(`✅ Migration report generated: ${reportPath}`)); return reportPath; } catch (error) { throw new Error(`Failed to generate migration report: ${error.message}`); } } async copyModFiles() { const sourceDir = path.join(__dirname, '..', 'powerpipe'); const targetDir = this.config.workspaceDir; try { const files = await fs.readdir(sourceDir); for (const file of files) { if (file.endsWith('.pp')) { const sourcePath = path.join(sourceDir, file); const targetPath = path.join(targetDir, file); await fs.copyFile(sourcePath, targetPath); } } } catch (error) { await this.createBasicModFiles(); } } async createBasicModFiles() { const modContent = ` mod "container_image_scanner" { title = "Container Image Scanner" description = "Analysis of container images and dependencies" } `; await fs.writeFile(path.join(this.config.workspaceDir, 'mod.pp'), modContent); } async createSQLiteDatabase() { if (!this.scanResults) return; const sqlite3 = require('sqlite3').verbose(); const dbPath = this.config.dbPath || path.join(this.config.workspaceDir, 'container_data.db'); return new Promise((resolve, reject) => { const db = new sqlite3.Database(dbPath); db.run(` CREATE TABLE IF NOT EXISTS container_images ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_name TEXT, image_tag TEXT, cluster_name TEXT, namespace TEXT, container_name TEXT, risk_level TEXT, pull_policy TEXT, last_updated TEXT, image_size INTEGER, security_context TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `, (err) => { if (err) { reject(err); return; } db.run('DELETE FROM container_images', (err) => { if (err) { reject(err); return; } const stmt = db.prepare(` INSERT INTO container_images ( image_name, image_tag, cluster_name, namespace, container_name, risk_level, pull_policy, last_updated, image_size, security_context ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); this.scanResults.images?.forEach((image) => { stmt.run([ image.name || image.image, image.tag, image.cluster, image.namespace, image.containerName || image.container, image.riskLevel, image.pullPolicy || null, image.lastUpdated || image.lastScanned || null, image.size || null, JSON.stringify(image.securityContext || {}) ]); }); stmt.finalize((err) => { if (err) { reject(err); } else { db.close(); resolve(undefined); } }); }); }); }); } createCustomDashboard(name, _filters) { return ` dashboard "${name}" { title = "Custom Container Analysis - ${name}" container { card { sql = "SELECT COUNT(*) as 'Total Images' FROM container_images" width = 3 } card { sql = "SELECT COUNT(DISTINCT cluster_name) as 'Clusters' FROM container_images" width = 3 } card { sql = "SELECT COUNT(*) as 'High Risk' FROM container_images WHERE risk_level IN ('CRITICAL', 'HIGH')" width = 3 } card { sql = "SELECT COUNT(*) as 'Bitnami Images' FROM container_images WHERE image_name LIKE '%bitnami%'" width = 3 } } container { table { title = "Container Images" sql = <<-EOQ SELECT image_name as "Image", image_tag as "Tag", cluster_name as "Cluster", namespace as "Namespace", risk_level as "Risk" FROM container_images ORDER BY CASE risk_level WHEN 'CRITICAL' THEN 1 WHEN 'HIGH' THEN 2 WHEN 'MEDIUM' THEN 3 ELSE 4 END LIMIT 50 EOQ width = 12 } } } `; } async exportToCSV(outputPath) { if (!this.scanResults?.images) return; const headers = ['Image', 'Tag', 'Cluster', 'Namespace', 'Container', 'Risk Level', 'Pull Policy', 'Last Updated']; const rows = [headers.join(',')]; this.scanResults.images.forEach((image) => { const row = [ image.name || image.image, image.tag, image.cluster, image.namespace, image.containerName || image.container, image.riskLevel, image.pullPolicy || '', image.lastUpdated || image.lastScanned || '' ]; rows.push(row.join(',')); }); await fs.writeFile(outputPath, rows.join('\n')); } async exportToJSON(outputPath) { if (!this.scanResults) return; await fs.writeFile(outputPath, JSON.stringify(this.scanResults, null, 2)); } async exportToSQL(outputPath) { if (!this.scanResults?.images) return; const sqlStatements = [ 'CREATE TABLE IF NOT EXISTS container_images (', ' image_name TEXT,', ' image_tag TEXT,', ' cluster_name TEXT,', ' namespace TEXT,', ' container_name TEXT,', ' risk_level TEXT,', ' pull_policy TEXT,', ' last_updated TEXT', ');', '' ]; this.scanResults.images.forEach((image) => { const values = [ `'${(image.name || image.image).replace(/'/g, "''")}'`, `'${image.tag.replace(/'/g, "''")}'`, `'${image.cluster.replace(/'/g, "''")}'`, `'${image.namespace.replace(/'/g, "''")}'`, `'${(image.containerName || image.container).replace(/'/g, "''")}'`, `'${image.riskLevel}'`, image.pullPolicy ? `'${image.pullPolicy}'` : 'NULL', (image.lastUpdated || image.lastScanned) ? `'${image.lastUpdated || image.lastScanned}'` : 'NULL' ]; sqlStatements.push(`INSERT INTO container_images VALUES (${values.join(', ')});`); }); await fs.writeFile(outputPath, sqlStatements.join('\n')); } async runPowerpipeCommand(args, cwd) { return new Promise((resolve, reject) => { const process = (0, child_process_1.spawn)('powerpipe', args, { cwd }); let output = ''; let error = ''; process.stdout?.on('data', (data) => { output += data.toString(); }); process.stderr?.on('data', (data) => { error += data.toString(); }); process.on('close', (code) => { if (code === 0) { resolve(output); } else { reject(new Error(`Powerpipe command failed: ${error}`)); } }); process.on('error', (err) => { reject(err); }); }); } } exports.PowerpipeIntegration = PowerpipeIntegration; async function checkPowerpipeInstallation() { try { const process = (0, child_process_1.spawn)('powerpipe', ['--version']); return new Promise((resolve) => { process.on('close', (code) => { resolve(code === 0); }); process.on('error', () => { resolve(false); }); }); } catch { return false; } } async function installPowerpipe() { console.log(chalk_1.default.blue('📦 Installing Powerpipe...')); try { await new Promise((resolve, reject) => { const process = (0, child_process_1.spawn)('brew', ['install', 'turbot/tap/powerpipe']); process.on('close', (code) => { if (code === 0) { resolve(undefined); } else { reject(new Error('Homebrew installation failed')); } }); process.on('error', reject); }); console.log(chalk_1.default.green('✅ Powerpipe installed successfully')); } catch { console.log(chalk_1.default.yellow('⚠️ Could not install Powerpipe automatically')); console.log(chalk_1.default.cyan('Please install Powerpipe manually:')); console.log(chalk_1.default.cyan(' macOS: brew install turbot/tap/powerpipe')); console.log(chalk_1.default.cyan(' Linux: See https://powerpipe.io/downloads')); throw new Error('Powerpipe installation required'); } }