Files
dockhand/routes/api/images/scan/+server.ts
Jarek Krochmalski 62e3c6439e Initial commit
2025-12-28 21:16:03 +01:00

150 lines
4.5 KiB
TypeScript

import { json, type RequestHandler } from '@sveltejs/kit';
import { scanImage, type ScanProgress, type ScanResult } from '$lib/server/scanner';
import { saveVulnerabilityScan, getLatestScanForImage } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
// Helper to convert ScanResult to database format
function scanResultToDbFormat(result: ScanResult, envId?: number) {
return {
environmentId: envId ?? null,
imageId: result.imageId || result.imageName, // Fallback to imageName if imageId is undefined
imageName: result.imageName,
scanner: result.scanner,
scannedAt: result.scannedAt,
scanDuration: result.scanDuration,
criticalCount: result.summary.critical,
highCount: result.summary.high,
mediumCount: result.summary.medium,
lowCount: result.summary.low,
negligibleCount: result.summary.negligible,
unknownCount: result.summary.unknown,
vulnerabilities: JSON.stringify(result.vulnerabilities),
error: result.error ?? null
};
}
// POST - Start a scan (returns SSE stream for progress)
export const POST: RequestHandler = async ({ request, url, cookies }) => {
const auth = await authorize(cookies);
const envIdParam = url.searchParams.get('env');
const envId = envIdParam ? parseInt(envIdParam) : undefined;
// Permission check with environment context (Scanning is an inspect operation)
if (auth.authEnabled && !await auth.can('images', 'inspect', envId)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
const body = await request.json();
const { imageName, scanner: forceScannerType } = body;
if (!imageName) {
return json({ error: 'Image name is required' }, { status: 400 });
}
// Create a readable stream for SSE
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
let controllerClosed = false;
const sendProgress = (progress: ScanProgress) => {
if (controllerClosed) return;
try {
const data = `data: ${JSON.stringify(progress)}\n\n`;
controller.enqueue(encoder.encode(data));
} catch {
controllerClosed = true;
}
};
// Send SSE keepalive comments every 5s to prevent Traefik timeout
const keepaliveInterval = setInterval(() => {
if (controllerClosed) return;
try {
controller.enqueue(encoder.encode(`: keepalive\n\n`));
} catch {
controllerClosed = true;
}
}, 5000);
try {
const results = await scanImage(imageName, envId, sendProgress, forceScannerType);
// Save results to database
for (const result of results) {
await saveVulnerabilityScan(scanResultToDbFormat(result, envId));
}
// Send final complete message with all results
sendProgress({
stage: 'complete',
message: `Scan complete - found ${results.reduce((sum, r) => sum + r.vulnerabilities.length, 0)} vulnerabilities`,
progress: 100,
result: results[0],
results: results // Include all scanner results
});
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
sendProgress({
stage: 'error',
message: `Scan failed: ${errorMsg}`,
error: errorMsg
});
} finally {
clearInterval(keepaliveInterval);
if (!controllerClosed) {
try {
controller.close();
} catch {
// Already closed
}
}
}
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
});
};
// GET - Get cached scan results for an image
export const GET: RequestHandler = async ({ url, cookies }) => {
const auth = await authorize(cookies);
const imageName = url.searchParams.get('image');
const envIdParam = url.searchParams.get('env');
const envId = envIdParam ? parseInt(envIdParam) : undefined;
const scanner = url.searchParams.get('scanner') as 'grype' | 'trivy' | undefined;
// Permission check with environment context
if (auth.authEnabled && !await auth.can('images', 'view', envId)) {
return json({ error: 'Permission denied' }, { status: 403 });
}
if (!imageName) {
return json({ error: 'Image name is required' }, { status: 400 });
}
try {
// Note: getLatestScanForImage signature is (imageId, scanner, environmentId)
const result = await getLatestScanForImage(imageName, scanner, envId);
if (!result) {
return json({ found: false });
}
return json({
found: true,
result
});
} catch (error) {
console.error('Failed to get scan results:', error);
return json({ error: 'Failed to get scan results' }, { status: 500 });
}
};