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 (
-
-
- );
-};
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 (
+
+
+ );
+};
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 (
- }
- type="submit"
- area-disabled={pending}
- >
- {buttonLabel}
-
- );
-};
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;