mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-07 21:29:06 +00:00
Initial commit
This commit is contained in:
149
routes/api/images/scan/+server.ts
Normal file
149
routes/api/images/scan/+server.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
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 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user