import { Blob } from 'node:buffer'; import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; const SILENCE_API_BASE = 'https://supreme.silenceai.net'; const REQUEST_TIMEOUT_MS = 30_000; interface StartScanResponse { scan_id?: unknown; } interface UploadReportResponse { report_id?: unknown; report_url?: unknown; } interface ExpiredResponse { error?: unknown; upgrade_url?: unknown; } interface UploadedReport { reportId: number; reportUrl: string; } interface FindingsSummary { totalFindings: number; criticalFindings: number; highFindings: number; mediumFindings: number; lowFindings: number; dockerfileCriticalFindings?: number; dockerfileHighFindings?: number; dockerfileMediumFindings?: number; dockerfileLowFindings?: number; } export class SilenceApiError extends Error { readonly kind: 'invalid_key' | 'subscription_expired' | 'http_error' | 'network_error'; readonly status?: number; readonly upgradeUrl?: string; constructor( message: string, kind: 'invalid_key' | 'subscription_expired' | 'http_error' | 'network_error', options?: { status?: number; upgradeUrl?: string }, ) { super(message); this.name = 'SilenceApiError'; this.kind = kind; this.status = options?.status; this.upgradeUrl = options?.upgradeUrl; } } async function readJsonSafe(response: Response): Promise { const text = await response.text(); if (text.trim().length === 0) { return null; } try { return JSON.parse(text) as unknown; } catch { return null; } } function asExpiredResponse(value: unknown): ExpiredResponse { if (value !== null && typeof value === 'object') { return value as ExpiredResponse; } return {}; } function ensureScanId(value: unknown): number { if (typeof value === 'number' && Number.isInteger(value) && value > 0) { return value; } throw new SilenceApiError('Unexpected scanner API response: scan_id is missing.', 'http_error'); } function ensureReportPayload(value: unknown): UploadedReport { if (value !== null && typeof value === 'object') { const payload = value as UploadReportResponse; if ( typeof payload.report_id === 'number' && Number.isInteger(payload.report_id) && payload.report_id > 0 && typeof payload.report_url === 'string' && payload.report_url.trim().length > 0 ) { return { reportId: payload.report_id, reportUrl: payload.report_url, }; } } throw new SilenceApiError('Unexpected scanner API response: report payload is invalid.', 'http_error'); } async function handleAuthErrors(response: Response): Promise { if (response.status === 401) { throw new SilenceApiError('Invalid API key.', 'invalid_key', { status: 401 }); } if (response.status === 403) { const body = asExpiredResponse(await readJsonSafe(response)); const upgradeUrl = typeof body.upgrade_url === 'string' ? body.upgrade_url : undefined; const errorText = typeof body.error === 'string' ? body.error.toLowerCase() : ''; if (errorText.includes('expired') || errorText.includes('inactive') || errorText === 'subscription_expired') { throw new SilenceApiError('Subscription expired.', 'subscription_expired', { status: 403, upgradeUrl, }); } throw new SilenceApiError('Scanner API rejected request.', 'http_error', { status: 403, upgradeUrl }); } } function authHeaders(key: string): Record { return { 'X-API-Key': key, }; } async function fetchWithTimeout(input: string, init: RequestInit): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { return await fetch(input, { ...init, signal: controller.signal }); } finally { clearTimeout(timeout); } } export async function startScan(key: string, projectName: string): Promise<{ scanId: number }> { let response: Response; try { response = await fetchWithTimeout(`${SILENCE_API_BASE}/api/v1/scanner/scan/start`, { method: 'POST', headers: { ...authHeaders(key), 'Content-Type': 'application/json', }, body: JSON.stringify({ project_name: projectName }), }); } catch (error) { throw new SilenceApiError( `Failed to reach scanner API: ${error instanceof Error ? error.message : String(error)}`, 'network_error', ); } await handleAuthErrors(response); if (!response.ok) { throw new SilenceApiError(`Scanner start failed with HTTP ${String(response.status)}.`, 'http_error', { status: response.status, }); } const payload = (await readJsonSafe(response)) as StartScanResponse | null; return { scanId: ensureScanId(payload?.scan_id) }; } export async function uploadReport( key: string, scanId: number, htmlPath: string, findings: FindingsSummary, ): Promise { let reportBytes: Buffer; try { reportBytes = await readFile(htmlPath); } catch (error) { throw new SilenceApiError( `Failed to read report file: ${error instanceof Error ? error.message : String(error)}`, 'network_error', ); } const reportName = basename(htmlPath); const formData = new FormData(); formData.append('report', new Blob([reportBytes], { type: 'text/html' }), reportName); formData.append('total_findings', String(findings.totalFindings)); formData.append('critical_findings', String(findings.criticalFindings)); formData.append('high_findings', String(findings.highFindings)); formData.append('medium_findings', String(findings.mediumFindings)); formData.append('low_findings', String(findings.lowFindings)); if (typeof findings.dockerfileCriticalFindings === 'number') { formData.append('dockerfile_critical_findings', String(findings.dockerfileCriticalFindings)); } if (typeof findings.dockerfileHighFindings === 'number') { formData.append('dockerfile_high_findings', String(findings.dockerfileHighFindings)); } if (typeof findings.dockerfileMediumFindings === 'number') { formData.append('dockerfile_medium_findings', String(findings.dockerfileMediumFindings)); } if (typeof findings.dockerfileLowFindings === 'number') { formData.append('dockerfile_low_findings', String(findings.dockerfileLowFindings)); } let response: Response; try { response = await fetchWithTimeout(`${SILENCE_API_BASE}/api/v1/scanner/scan/${String(scanId)}/report`, { method: 'POST', headers: authHeaders(key), body: formData, }); } catch (error) { throw new SilenceApiError( `Failed to upload report: ${error instanceof Error ? error.message : String(error)}`, 'network_error', ); } await handleAuthErrors(response); if (!response.ok) { throw new SilenceApiError(`Report upload failed with HTTP ${String(response.status)}.`, 'http_error', { status: response.status, }); } const payload = await readJsonSafe(response); return ensureReportPayload(payload); }