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.

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'); } }