import { getComplianceCsv, getComplianceOcsf, getCompliancePdfReport, type ScanBinaryResult, } from "@/actions/scans"; import { getTask } from "@/actions/task"; import { auth } from "@/auth.config"; import { useToast } from "@/components/shadcn"; import { COMPLIANCE_REPORT_DISPLAY_NAMES, type ComplianceReportType, } from "@/lib/compliance/compliance-report-types"; import { readEnv } from "@/lib/runtime-env"; import { AuthSocialProvider, MetaDataProps, PermissionInfo } from "@/types"; export const baseUrl = process.env.AUTH_URL; export const apiBaseUrl = readEnv( "UI_API_BASE_URL", "NEXT_PUBLIC_API_BASE_URL", ); /** * Extracts a form value from a FormData object * @param formData - The FormData object to extract from * @param field - The name of the field to extract * @returns The value of the field */ export const getFormValue = (formData: FormData, field: string) => formData.get(field); /** * Filters out empty values from an object * @param obj - Object to filter * @returns New object with empty values removed * Avoids sending empty values to the API */ export function filterEmptyValues( obj: Record, ): Record { return Object.fromEntries( Object.entries(obj).filter(([_, value]) => { // Keep number 0 and boolean false as they are valid values if (value === 0 || value === false) return true; // Filter out null, undefined, empty strings, and empty arrays if (value === null || value === undefined) return false; if (typeof value === "string" && value.trim() === "") return false; if (Array.isArray(value) && value.length === 0) return false; return true; }), ); } /** * Returns the authentication headers for API requests * @param options - Optional configuration options * @returns Authentication headers with Accept and Authorization */ export const getAuthHeaders = async (options?: { contentType?: boolean }) => { const session = await auth(); const headers: Record = { Accept: "application/vnd.api+json", Authorization: `Bearer ${session?.accessToken}`, }; if (options?.contentType) { headers["Content-Type"] = "application/vnd.api+json"; } return headers; }; export const getAuthUrl = (provider: AuthSocialProvider) => { const config = { google: { baseUrl: "https://accounts.google.com/o/oauth2/v2/auth", params: { redirect_uri: process.env.SOCIAL_GOOGLE_OAUTH_CALLBACK_URL, prompt: "consent", response_type: "code", client_id: process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_ID, scope: "openid email profile", access_type: "offline", }, }, github: { baseUrl: "https://github.com/login/oauth/authorize", params: { client_id: process.env.SOCIAL_GITHUB_OAUTH_CLIENT_ID, redirect_uri: process.env.SOCIAL_GITHUB_OAUTH_CALLBACK_URL, scope: "user:email", }, }, }; const { baseUrl, params } = config[provider]; const url = new URL(baseUrl); Object.entries(params).forEach(([key, value]) => { url.searchParams.set(key, value || ""); }); return url.toString(); }; const REPORT_PREPARATION_ERROR = "Unable to prepare the scan report. Please try again in a few minutes."; export const GENERIC_SERVER_ERROR_MESSAGE = "Server is temporarily unavailable. Please try again in a few minutes."; const HTML_ERROR_PATTERN = /(?: { const trimmedMessage = message.trim(); if (!trimmedMessage) { return fallback; } return HTML_ERROR_PATTERN.test(trimmedMessage) ? fallback : trimmedMessage; }; const getPreflightErrorMessage = async (response: Response) => { const contentType = response.headers.get("content-type")?.toLowerCase() || ""; if (contentType.includes("text/html")) { return REPORT_PREPARATION_ERROR; } return (await response.text()) || "An unknown error occurred."; }; export const downloadScanZip = async ( scanId: string, toast: ReturnType["toast"], ) => { const reportUrl = `/api/scans/${encodeURIComponent(scanId)}/report`; try { const preflightResponse = await fetch(`${reportUrl}?preflight=1`, { cache: "no-store", }); if (preflightResponse.status === 202) { toast({ title: "The report is still being generated", description: "Please try again in a few minutes.", }); return; } if (!preflightResponse.ok) { toast({ variant: "destructive", title: "Download Failed", description: await getPreflightErrorMessage(preflightResponse), }); return; } } catch (_error) { toast({ variant: "destructive", title: "Download Failed", description: "Unable to start the report download. Please try again.", }); return; } const a = document.createElement("a"); a.href = reportUrl; a.download = `scan-${scanId}-report.zip`; document.body.appendChild(a); a.click(); document.body.removeChild(a); toast({ title: "Download Started", description: "Your browser is downloading the scan report.", }); }; /** * Generic function to download a file from base64 data. * * Exported so feature-local wrappers (e.g. the cross-provider PDF download in * app/(prowler)/compliance/_lib) reuse the base64→blob handling instead of * duplicating it; those wrappers cannot live here because their server * actions import the @/lib barrel, which would create a cycle. */ export const downloadFile = async ( result: ScanBinaryResult, outputType: string, successMessage: string, toast: ReturnType["toast"], ): Promise => { if ("pending" in result && result.pending) { toast({ title: "The report is still being generated", description: "Please try again in a few minutes.", }); return; } if ("success" in result && result.success) { try { const binaryString = window.atob(result.data); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } const blob = new Blob([bytes], { type: outputType }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = result.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); toast({ title: "Download Complete", description: successMessage, }); } catch (_error) { toast({ variant: "destructive", title: "Download Failed", description: "An error occurred while processing the file.", }); } return; } if ("error" in result) { toast({ variant: "destructive", title: "Download Failed", description: result.error, }); return; } // Unexpected case toast({ variant: "destructive", title: "Download Failed", description: "Unexpected response. Please try again later.", }); }; export const downloadComplianceCsv = async ( scanId: string, complianceId: string, toast: ReturnType["toast"], ): Promise => { toast({ title: "Download Started", description: "Preparing the CSV report. This may take a moment.", }); const result = await getComplianceCsv(scanId, complianceId); await downloadFile( result, "text/csv", "The compliance report has been downloaded successfully.", toast, ); }; /** * Download the per-framework OCSF JSON export. * * Only universal frameworks declaring an ``outputs`` block produce this * artifact (currently DORA and CSA CCM 4.0); callers must gate the call * via ``isOcsfSupported`` to avoid surfacing a broken download on * frameworks the API will 404 on. */ export const downloadComplianceOcsf = async ( scanId: string, complianceId: string, toast: ReturnType["toast"], ): Promise => { toast({ title: "Download Started", description: "Preparing the OCSF report. This may take a moment.", }); const result = await getComplianceOcsf(scanId, complianceId); await downloadFile( result, "application/json", "The compliance OCSF report has been downloaded successfully.", toast, ); }; /** * Download a compliance PDF report. * * The call hits ``/scans/{id}/{reportType}`` for every supported framework. * For CIS — which ships multiple versions per provider — the backend only * generates the PDF for the highest available version, so the call site * does not need to resolve a specific variant. * * @param scanId - The scan ID * @param reportType - Type of report (from COMPLIANCE_REPORT_TYPES) * @param toast - Toast notification function */ export const downloadCompliancePdf = async ( scanId: string, reportType: ComplianceReportType, toast: ReturnType["toast"], ): Promise => { const reportName = COMPLIANCE_REPORT_DISPLAY_NAMES[reportType]; toast({ title: "Download Started", description: `Preparing the ${reportName} PDF report. This may take a moment.`, }); const result = await getCompliancePdfReport(scanId, reportType); await downloadFile( result, "application/pdf", `The ${reportName} PDF report has been downloaded successfully.`, toast, ); }; /** * @deprecated Use {@link downloadCompliancePdf} instead. Kept as a thin * wrapper for callers not yet migrated. */ export const downloadComplianceReportPdf = async ( scanId: string, reportType: ComplianceReportType, toast: ReturnType["toast"], ): Promise => downloadCompliancePdf(scanId, reportType, toast); export const isGoogleOAuthEnabled = !!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_ID && !!process.env.SOCIAL_GOOGLE_OAUTH_CLIENT_SECRET; export const isGithubOAuthEnabled = !!process.env.SOCIAL_GITHUB_OAUTH_CLIENT_ID && !!process.env.SOCIAL_GITHUB_OAUTH_CLIENT_SECRET; export const checkTaskStatus = async ( taskId: string, maxRetries: number = 20, retryDelay: number = 1500, ): Promise<{ completed: boolean; error?: string }> => { for (let attempt = 0; attempt < maxRetries; attempt++) { const task = await getTask(taskId); if (task.error) { console.error(`Error retrieving task: ${task.error}`); return { completed: false, error: task.error }; } const state = task.data.attributes.state; switch (state) { case "completed": return { completed: true }; case "failed": return { completed: false, error: task.data.attributes.result.error }; case "available": case "scheduled": case "executing": // Continue waiting if the task is still in progress await new Promise((resolve) => setTimeout(resolve, retryDelay)); break; default: return { completed: false, error: "Unexpected task state" }; } } return { completed: false, error: "Max retries exceeded" }; }; export const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value)); export const convertFileToUrl = (file: File) => URL.createObjectURL(file); export const getPaginationInfo = (metadata: MetaDataProps) => { const currentPage = metadata?.pagination.page ?? "1"; const totalPages = metadata?.pagination.pages; const totalEntries = metadata?.pagination.count; const itemsPerPageOptions = metadata?.pagination.itemsPerPage ?? [ 10, 20, 30, 50, 100, ]; return { currentPage, totalPages, totalEntries, itemsPerPageOptions }; }; export function encryptKey(passkey: string) { return btoa(passkey); } export function decryptKey(passkey: string) { return atob(passkey); } export const getErrorMessage = (error: unknown): string => { if (error instanceof Error) { return sanitizeErrorMessage(error.message); } else if (error && typeof error === "object" && "message" in error) { return sanitizeErrorMessage(String(error.message)); } else if (typeof error === "string") { return sanitizeErrorMessage(error); } else { return "Oops! Something went wrong."; } }; export const permissionFormFields: PermissionInfo[] = [ { field: "manage_users", label: "Invite and Manage Users", description: "Allows inviting new users and managing existing user details", }, { field: "manage_account", label: "Manage Account", description: "Provides access to account settings and RBAC configuration", }, { field: "unlimited_visibility", label: "Unlimited Visibility", description: "Grants organization-wide visibility across all providers, resources, findings, scans, and compliance results without granting admin actions.", }, { field: "manage_providers", label: "Manage Providers", description: "Allows configuration and management of provider connections", }, { field: "manage_integrations", label: "Manage Integrations", description: "Allows configuration and management of third-party integrations", }, { field: "manage_scans", label: "Manage Scans", description: "Allows launching and configuring scans security scans", }, { field: "manage_alerts", label: "Manage Alerts", description: "Allows creating and managing custom alerts", }, { field: "manage_billing", label: "Manage Billing", description: "Provides access to billing settings and invoices", }, ];