diff --git a/app/(prowler)/scans/page.tsx b/app/(prowler)/scans/page.tsx index be74f9a28a..4ea3db2b76 100644 --- a/app/(prowler)/scans/page.tsx +++ b/app/(prowler)/scans/page.tsx @@ -1,30 +1,43 @@ -import { Link, Spacer, Tooltip } from "@nextui-org/react"; +import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; import { getScans } from "@/actions/scans"; -import { FilterControls, filterScans } from "@/components/filters"; -import { InfoIcon } from "@/components/icons"; +import { filterScans } from "@/components/filters"; +import { LaunchScanWorkflow } from "@/components/scans/launch-workflow"; import { SkeletonTableScans } from "@/components/scans/table"; -import { ColumnProviderScans } from "@/components/scans/table/provider-scans"; import { ColumnGetScans } from "@/components/scans/table/scans"; import { Header } from "@/components/ui"; import { DataTable } from "@/components/ui/table"; -import { SearchParamsProps } from "@/types"; +import { ProviderProps, SearchParamsProps } from "@/types"; export default async function Scans({ searchParams, }: { searchParams: SearchParamsProps; }) { - const searchParamsKey = JSON.stringify(searchParams || {}); + const filteredParams = { ...searchParams }; + delete filteredParams.scanId; + const searchParamsKey = JSON.stringify(filteredParams); + + const providersData = await getProviders({}); + + const providerInfo = providersData?.data?.length + ? providersData.data.map((provider: ProviderProps) => ({ + providerId: provider.id, + alias: provider.attributes.alias, + providerType: provider.attributes.provider, + uid: provider.attributes.uid, + connected: provider.attributes.connection.connected, + })) + : []; return ( <>
- +
@@ -33,43 +46,11 @@ export default async function Scans({
-
- }> - - -
); } -const SSRDataTableProviders = async () => { - const filters = { "filter[connected]": "true" }; - const providersData = await getProviders({ page: 1, filters }); - return ( - <> -
- - - -

Connected providers

-
- -

- If you don't see any providers, please check your connection settings on - the{" "} - - providers page - - . -

- - ); -}; - const SSRDataTableScans = async ({ searchParams, }: { @@ -78,9 +59,11 @@ const SSRDataTableScans = async ({ const page = parseInt(searchParams.page?.toString() || "1", 10); const sort = searchParams.sort?.toString(); - // Extract all filter parameters + // Extract all filter parameters, excluding scanId const filters = Object.fromEntries( - Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + Object.entries(searchParams).filter( + ([key]) => key.startsWith("filter[") && key !== "scanId", + ), ); // Extract query from filters diff --git a/components/filters/data-filters.ts b/components/filters/data-filters.ts index 2b2d319783..673e4f0fff 100644 --- a/components/filters/data-filters.ts +++ b/components/filters/data-filters.ts @@ -8,6 +8,11 @@ export const filterProviders = [ ]; export const filterScans = [ + { + key: "provider_type__in", + labelCheckboxGroup: "Provider", + values: ["aws", "azure", "gcp", "kubernetes"], + }, { key: "state", labelCheckboxGroup: "State", diff --git a/components/scans/forms/index.ts b/components/scans/forms/index.ts index 59deb9251a..4acd302cb9 100644 --- a/components/scans/forms/index.ts +++ b/components/scans/forms/index.ts @@ -1,3 +1,2 @@ export * from "./edit-scan-form"; -export * from "./scan-on-demand-form"; export * from "./schedule-form"; diff --git a/components/scans/forms/scan-on-demand-form.tsx b/components/scans/forms/scan-on-demand-form.tsx deleted file mode 100644 index a805e55ac5..0000000000 --- a/components/scans/forms/scan-on-demand-form.tsx +++ /dev/null @@ -1,126 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { RocketIcon } from "lucide-react"; -import { Dispatch, SetStateAction } from "react"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; - -import { scanOnDemand } from "@/actions/scans"; -import { useToast } from "@/components/ui"; -import { CustomButton, CustomInput } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { onDemandScanFormSchema } from "@/types"; - -export const ScanOnDemandForm = ({ - providerId, - scanName, - scannerArgs, - setIsOpen, -}: { - providerId: string; - scanName?: string; - scannerArgs?: { checksToExecute: string[] }; - setIsOpen: Dispatch>; -}) => { - const formSchema = onDemandScanFormSchema(); - - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId: providerId, - scanName: scanName, - scannerArgs: scannerArgs, - }, - }); - - const { toast } = useToast(); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: z.infer) => { - const formData = new FormData(); - - // Loop through form values and add to formData, converting objects to JSON strings - Object.entries(values).forEach( - ([key, value]) => - value !== undefined && - formData.append( - key, - typeof value === "object" ? JSON.stringify(value) : value, - ), - ); - - const data = await scanOnDemand(formData); - - if (data?.errors && data.errors.length > 0) { - const error = data.errors[0]; - const errorMessage = `${error.detail}`; - // show error - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } else { - toast({ - title: "Success!", - description: "The scan was launched successfully.", - }); - setIsOpen(false); - } - }; - - return ( -
- - - -
- -
- -
- setIsOpen(false)} - isDisabled={isLoading} - > - Cancel - - - } - > - {isLoading ? <>Loading : Start now} - -
-
- - ); -}; diff --git a/components/scans/forms/schedule-form.tsx b/components/scans/forms/schedule-form.tsx index 30a8982856..2b75a3c263 100644 --- a/components/scans/forms/schedule-form.tsx +++ b/components/scans/forms/schedule-form.tsx @@ -95,7 +95,7 @@ export const ScheduleForm = ({ { + const formSchema = onDemandScanFormSchema(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + providerId: "", + scanName: "", + scannerArgs: undefined, + }, + }); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: z.infer) => { + const formData = new FormData(); + console.log(values); + + // Loop through form values and add to formData + Object.entries(values).forEach( + ([key, value]) => + value !== undefined && + formData.append( + key, + typeof value === "object" ? JSON.stringify(value) : value, + ), + ); + + const data = await scanOnDemand(formData); + + if (data?.errors && data.errors.length > 0) { + const error = data.errors[0]; + const errorMessage = `${error.detail}`; + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } else { + toast({ + title: "Success!", + description: "The scan was launched successfully.", + }); + // Reset form after successful submission + form.reset(); + } + }; + + return ( +
+ +
+
+
+

+ Launch Scan +

+ +
+ + + {form.watch("providerId") && ( + + + + )} + +
+ +
+ + {form.watch("providerId") && ( + + + + )} + +
+ + + {form.watch("providerId") && ( + + form.reset()} + className="w-fit border-gray-200 bg-transparent" + ariaLabel="Clear form" + variant="bordered" + size="lg" + radius="lg" + > + Cancel + + } + isDisabled={true} + > + {isLoading ? <>Loading : Schedule} + + + } + > + {isLoading ? <>Loading : Start now} + + + )} + +
+
+ + ); +}; diff --git a/components/scans/launch-workflow/select-scan-provider.tsx b/components/scans/launch-workflow/select-scan-provider.tsx new file mode 100644 index 0000000000..c1805f488b --- /dev/null +++ b/components/scans/launch-workflow/select-scan-provider.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Select, SelectItem } from "@nextui-org/react"; +import { Control, FieldPath, FieldValues } from "react-hook-form"; + +import { AWSProviderBadge } from "@/components/icons/providers-badge/AWSProviderBadge"; +import { AzureProviderBadge } from "@/components/icons/providers-badge/AzureProviderBadge"; +import { GCPProviderBadge } from "@/components/icons/providers-badge/GCPProviderBadge"; +import { KS8ProviderBadge } from "@/components/icons/providers-badge/KS8ProviderBadge"; +import { FormControl, FormField, FormMessage } from "@/components/ui/form"; + +interface SelectScanProviderProps< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> { + providers: { + providerId: string; + alias: string; + providerType: string; + uid: string; + connected: boolean; + }[]; + control: Control; + name: TName; +} + +export const SelectScanProvider = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + providers, + control, + name, +}: SelectScanProviderProps) => { + const renderBadge = (providerType: string) => { + switch (providerType) { + case "aws": + return ; + case "azure": + return ; + case "gcp": + return ; + case "kubernetes": + return ; + default: + return null; + } + }; + + return ( + ( + <> + + + + + + )} + /> + ); +}; diff --git a/components/scans/table/provider-scans/column-provider-scans.tsx b/components/scans/table/provider-scans/column-provider-scans.tsx deleted file mode 100644 index 938c3c1681..0000000000 --- a/components/scans/table/provider-scans/column-provider-scans.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; - -import { EntityInfoShort } from "@/components/ui/entities"; -import { ProviderProps } from "@/types"; - -import { DataTableRowActions } from "./data-table-row-actions"; - -const getProviderData = (row: { original: ProviderProps }) => { - return row.original; -}; - -export const ColumnProviderScans: ColumnDef[] = [ - { - accessorKey: "provider", - header: "Provider", - cell: ({ row }) => { - const { - attributes: { connection, provider, alias, uid }, - } = getProviderData(row); - return ( - - ); - }, - }, - { - accessorKey: "launchScan", - header: "Launch Scan", - cell: ({ row }) => { - return ; - }, - }, -]; diff --git a/components/scans/table/provider-scans/data-table-row-actions.tsx b/components/scans/table/provider-scans/data-table-row-actions.tsx deleted file mode 100644 index 8cad2b0f7f..0000000000 --- a/components/scans/table/provider-scans/data-table-row-actions.tsx +++ /dev/null @@ -1,110 +0,0 @@ -"use client"; - -import { - Dropdown, - DropdownItem, - DropdownMenu, - DropdownSection, - DropdownTrigger, -} from "@nextui-org/react"; -import { Row } from "@tanstack/react-table"; -import { CalendarClockIcon, RocketIcon } from "lucide-react"; -import { useState } from "react"; - -import { AddIcon } from "@/components/icons"; -import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; - -import { ScanOnDemandForm, ScheduleForm } from "../../forms"; - -interface DataTableRowActionsProps { - row: Row; -} -const iconClasses = - "text-2xl text-default-500 pointer-events-none flex-shrink-0"; - -export function DataTableRowActions({ - row, -}: DataTableRowActionsProps) { - const [isScanOnDemandOpen, setIsScanOnDemandOpen] = useState(false); - const [isScanScheduleOpen, setIsScanScheduleOpen] = useState(false); - - const providerId = (row.original as { id: string }).id; - const scanName = (row.original as any).attributes?.name; - return ( - <> - - - - - - - - -
- - - } - > - Start - - - - - } - onClick={() => setIsScanOnDemandOpen(true)} - > - Start now - - - - } - onClick={() => setIsScanScheduleOpen(true)} - > - Schedule Scan - - - - -
- - ); -} diff --git a/components/scans/table/provider-scans/index.ts b/components/scans/table/provider-scans/index.ts deleted file mode 100644 index 415bafad0d..0000000000 --- a/components/scans/table/provider-scans/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./column-provider-scans"; -export * from "./data-table-row-actions"; diff --git a/components/scans/table/scan-detail.tsx b/components/scans/table/scan-detail.tsx index 5dd8dc2547..c95e28fcfc 100644 --- a/components/scans/table/scan-detail.tsx +++ b/components/scans/table/scan-detail.tsx @@ -4,16 +4,23 @@ import { Card, CardBody, CardHeader, Divider } from "@nextui-org/react"; import { DateWithTime, SnippetId } from "@/components/ui/entities"; import { StatusBadge } from "@/components/ui/table/status-badge"; -import { ScanProps } from "@/types"; +import { ScanProps, TaskDetails } from "@/types"; -export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => { +interface ScanDetailsProps { + scanDetails: ScanProps & { + taskDetails?: TaskDetails; + }; +} + +export const ScanDetail = ({ scanDetails }: ScanDetailsProps) => { const scanOnDemand = scanDetails.attributes; + const taskDetails = scanDetails.taskDetails; + return (
-

Scan Details -

-

{scanOnDemand.name}

+

Scan Details

@@ -22,6 +29,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
+ } @@ -99,7 +107,7 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
-

Scan Arguments

+

Scan arguments

@@ -115,6 +123,47 @@ export const ScanDetail = ({ scanDetails }: { scanDetails: ScanProps }) => {
+ {taskDetails && ( + + +

State details

+
+ + +
+ + + + {taskDetails.attributes.result && ( + <> + + {taskDetails.attributes.result.exc_message && ( + + )} + + )} + + +
+
+
+ )}
); }; diff --git a/components/scans/table/scans/column-get-scans.tsx b/components/scans/table/scans/column-get-scans.tsx index 930f189de5..3e2d940e8e 100644 --- a/components/scans/table/scans/column-get-scans.tsx +++ b/components/scans/table/scans/column-get-scans.tsx @@ -1,6 +1,7 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; +import { useSearchParams } from "next/navigation"; import { InfoIcon } from "@/components/icons"; import { DateWithTime, EntityInfoShort } from "@/components/ui/entities"; @@ -125,11 +126,16 @@ export const ColumnGetScans: ColumnDef[] = [ id: "moreInfo", header: "Details", cell: ({ row }) => { + const searchParams = useSearchParams(); + const scanId = searchParams.get("scanId"); + const isOpen = scanId === row.original.id; + return ( } title="Scan Details" description="View the scan details" + defaultOpen={isOpen} > diff --git a/components/scans/table/scans/data-table-row-details.tsx b/components/scans/table/scans/data-table-row-details.tsx index 997fa0e5d9..92adbbe18c 100644 --- a/components/scans/table/scans/data-table-row-details.tsx +++ b/components/scans/table/scans/data-table-row-details.tsx @@ -1,22 +1,56 @@ "use client"; +import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import { getScan } from "@/actions/scans"; +import { getTask } from "@/actions/task"; import { ScanDetail, SkeletonTableScans } from "@/components/scans/table"; +import { checkTaskStatus } from "@/lib"; import { ScanProps } from "@/types"; export const DataTableRowDetails = ({ entityId }: { entityId: string }) => { + const router = useRouter(); + const searchParams = useSearchParams(); const [scanDetails, setScanDetails] = useState(null); const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + // Add scanId to URL + const params = new URLSearchParams(searchParams.toString()); + params.set("scanId", entityId); + router.push(`?${params.toString()}`, { scroll: false }); + + // Cleanup function: remove scanId from URL when component unmounts + return () => { + const newParams = new URLSearchParams(searchParams.toString()); + newParams.delete("scanId"); + router.push(`?${newParams.toString()}`, { scroll: false }); + }; + }, [entityId, router, searchParams]); + useEffect(() => { const fetchScanDetails = async () => { try { const result = await getScan(entityId); - setScanDetails(result?.data); + + const taskId = result.data.relationships.task?.data?.id; + + if (taskId) { + const taskResult = await checkTaskStatus(taskId); + + if (taskResult.completed !== undefined) { + const task = await getTask(taskId); + setScanDetails({ + ...result.data, + taskDetails: task.data, + }); + } + } else { + setScanDetails(result.data); + } } catch (error) { - console.error("Error fetching scan details:", error); + console.error("Error in fetchScanDetails:", error); } finally { setIsLoading(false); } diff --git a/components/ui/custom/CustomButtonClientAction.tsx b/components/ui/custom/CustomButtonClientAction.tsx deleted file mode 100644 index bafc945e17..0000000000 --- a/components/ui/custom/CustomButtonClientAction.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client"; - -import { Button, CircularProgress } from "@nextui-org/react"; -import clsx from "clsx"; -import { useFormStatus } from "react-dom"; - -interface CustomButtonClientActionProps { - buttonLabel: string; - danger?: boolean; -} - -export const CustomButtonClientAction = ({ - buttonLabel, - danger = false, -}: CustomButtonClientActionProps) => { - const { pending } = useFormStatus(); - - return ( - - ); -}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 130646a89e..58a00e4d1d 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -5,4 +5,3 @@ export * from "./custom-dropdown-filter"; export * from "./custom-input"; export * from "./custom-loader"; export * from "./custom-radio"; -export * from "./CustomButtonClientAction"; diff --git a/components/ui/sheet/trigger-sheet.tsx b/components/ui/sheet/trigger-sheet.tsx index 2275577cbd..992b3ac098 100644 --- a/components/ui/sheet/trigger-sheet.tsx +++ b/components/ui/sheet/trigger-sheet.tsx @@ -12,6 +12,7 @@ interface TriggerSheetProps { title: string; description: string; children: React.ReactNode; + defaultOpen?: boolean; } export function TriggerSheet({ @@ -19,13 +20,14 @@ export function TriggerSheet({ title, description, children, + defaultOpen = false, }: TriggerSheetProps) { return ( - + {triggerComponent} - + {title} {description} diff --git a/types/components.ts b/types/components.ts index b6c4b59da2..60fe123b51 100644 --- a/types/components.ts +++ b/types/components.ts @@ -26,6 +26,22 @@ export type NextUIColors = | "danger" | "default"; +export interface TaskDetails { + attributes: { + state: string; + completed_at: string; + result: { + exc_type?: string; + exc_message?: string[]; + exc_module?: string; + }; + task_args: { + scan_id: string; + provider_id: string; + checks_to_execute: string[]; + }; + }; +} export type AWSCredentials = { aws_access_key_id: string; aws_secret_access_key: string;