mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-03 05:29:05 +00:00
150 lines
4.5 KiB
TypeScript
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 });
|
|
}
|
|
};
|