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