diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 3e463cfe60..4bf3030402 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,7 +1,3 @@
-### Motivation
-
-Why it this PR useful for the project?
-
### Description
What was done in this PR
diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts
index 4578786534..cf079892b4 100644
--- a/actions/providers/providers.ts
+++ b/actions/providers/providers.ts
@@ -4,7 +4,7 @@ import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { auth } from "@/auth.config";
-import { getErrorMessage, parseStringify } from "@/lib";
+import { getErrorMessage, parseStringify, wait } from "@/lib";
export const getProviders = async ({
page = 1,
@@ -114,7 +114,7 @@ export const addProvider = async (formData: FormData) => {
const keyServer = process.env.API_BASE_URL;
const providerType = formData.get("providerType");
- const providerId = formData.get("providerId");
+ const providerUid = formData.get("providerUid");
const providerAlias = formData.get("providerAlias");
const url = new URL(`${keyServer}/providers`);
@@ -132,7 +132,7 @@ export const addProvider = async (formData: FormData) => {
type: "Provider",
attributes: {
provider: providerType,
- uid: providerId,
+ uid: providerUid,
alias: providerAlias,
},
},
@@ -149,11 +149,109 @@ export const addProvider = async (formData: FormData) => {
}
};
+export const addCredentialsProvider = async (formData: FormData) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers/secrets`);
+
+ const secretName = formData.get("secretName");
+ const providerId = formData.get("providerId");
+ const providerType = formData.get("providerType");
+
+ const isRole = formData.get("role_arn") !== null;
+
+ let secret = {};
+ let secretType = "static"; // Default to static credentials
+
+ if (providerType === "aws") {
+ if (isRole) {
+ // Role-based configuration for AWS
+ secretType = "role";
+ secret = {
+ role_arn: formData.get("role_arn"),
+ aws_access_key_id: formData.get("aws_access_key_id") || undefined,
+ aws_secret_access_key:
+ formData.get("aws_secret_access_key") || undefined,
+ aws_session_token: formData.get("aws_session_token") || undefined,
+ session_duration:
+ parseInt(formData.get("session_duration") as string, 10) || 3600,
+ external_id: formData.get("external_id") || undefined,
+ role_session_name: formData.get("role_session_name") || undefined,
+ };
+ } else {
+ // Static credentials configuration for AWS
+ secret = {
+ aws_access_key_id: formData.get("aws_access_key_id"),
+ aws_secret_access_key: formData.get("aws_secret_access_key"),
+ aws_session_token: formData.get("aws_session_token") || undefined,
+ };
+ }
+ } else if (providerType === "azure") {
+ // Static credentials configuration for Azure
+ secret = {
+ client_id: formData.get("client_id"),
+ client_secret: formData.get("client_secret"),
+ tenant_id: formData.get("tenant_id"),
+ };
+ } else if (providerType === "gcp") {
+ // Static credentials configuration for GCP
+ secret = {
+ client_id: formData.get("client_id"),
+ client_secret: formData.get("client_secret"),
+ refresh_token: formData.get("refresh_token"),
+ };
+ } else if (providerType === "kubernetes") {
+ // Static credentials configuration for Kubernetes
+ secret = {
+ kubeconfig_content: formData.get("kubeconfig_content"),
+ };
+ }
+
+ const bodyData = {
+ data: {
+ type: "ProviderSecret",
+ attributes: {
+ secret_type: secretType,
+ secret,
+ name: secretName,
+ },
+ relationships: {
+ provider: {
+ data: {
+ id: providerId,
+ type: "Provider",
+ },
+ },
+ },
+ },
+ };
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/vnd.api+json",
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ body: JSON.stringify(bodyData),
+ });
+ const data = await response.json();
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ console.error(error);
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
export const checkConnectionProvider = async (formData: FormData) => {
- // const session = await auth();
+ const session = await auth();
const keyServer = process.env.API_BASE_URL;
- const providerId = formData.get("id");
+ const providerId = formData.get("providerId");
const url = new URL(`${keyServer}/providers/${providerId}/connection`);
@@ -162,6 +260,30 @@ export const checkConnectionProvider = async (formData: FormData) => {
method: "POST",
headers: {
Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ await wait(1000);
+ revalidatePath("/providers");
+ return parseStringify(data);
+ } catch (error) {
+ return {
+ error: getErrorMessage(error),
+ };
+ }
+};
+
+export const deleteCredentials = async (secretId: string) => {
+ const session = await auth();
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/providers/secrets/${secretId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${session?.accessToken}`,
},
});
const data = await response.json();
@@ -190,6 +312,7 @@ export const deleteProvider = async (formData: FormData) => {
},
});
const data = await response.json();
+ await wait(1000);
revalidatePath("/providers");
return parseStringify(data);
} catch (error) {
diff --git a/actions/task/index.ts b/actions/task/index.ts
new file mode 100644
index 0000000000..078ba8fc62
--- /dev/null
+++ b/actions/task/index.ts
@@ -0,0 +1 @@
+export * from "./tasks";
diff --git a/actions/task/tasks.ts b/actions/task/tasks.ts
new file mode 100644
index 0000000000..fdf75c7cea
--- /dev/null
+++ b/actions/task/tasks.ts
@@ -0,0 +1,24 @@
+"use server";
+
+import { auth } from "@/auth.config";
+import { getErrorMessage, parseStringify } from "@/lib";
+
+export const getTask = async (taskId: string) => {
+ const session = await auth();
+
+ const keyServer = process.env.API_BASE_URL;
+ const url = new URL(`${keyServer}/tasks/${taskId}`);
+
+ try {
+ const response = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/vnd.api+json",
+ Authorization: `Bearer ${session?.accessToken}`,
+ },
+ });
+ const data = await response.json();
+ return parseStringify(data);
+ } catch (error) {
+ return { error: getErrorMessage(error) };
+ }
+};
diff --git a/app/(prowler)/findings/page.tsx b/app/(prowler)/findings/page.tsx
index f75638f792..1c277cad0c 100644
--- a/app/(prowler)/findings/page.tsx
+++ b/app/(prowler)/findings/page.tsx
@@ -18,6 +18,8 @@ export default async function Findings({
}: {
searchParams: SearchParamsProps;
}) {
+ const searchParamsKey = JSON.stringify(searchParams || {});
+
return (
<>
@@ -25,7 +27,7 @@ export default async function Findings({
- }>
+ }>
>
diff --git a/app/(prowler)/page.tsx b/app/(prowler)/page.tsx
index 261ca1c00f..335b53b4f4 100644
--- a/app/(prowler)/page.tsx
+++ b/app/(prowler)/page.tsx
@@ -1,17 +1,13 @@
import { Spacer } from "@nextui-org/react";
-import { SeverityChart, StatusChart } from "@/components/charts";
-import { AttackSurface } from "@/components/overview";
import { Header } from "@/components/ui";
-import { CustomBox } from "@/components/ui/custom";
export default function Home() {
return (
<>
-
-
-
*/}
>
);
}
diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx
new file mode 100644
index 0000000000..6a525990fa
--- /dev/null
+++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx
@@ -0,0 +1,35 @@
+import { redirect } from "next/navigation";
+import React from "react";
+
+import {
+ ViaCredentialsForm,
+ ViaRoleForm,
+} from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string; via?: string };
+}
+
+export default function AddCredentialsPage({ searchParams }: Props) {
+ if (
+ !searchParams.type ||
+ !searchParams.id ||
+ (searchParams.type === "aws" && !searchParams.via)
+ ) {
+ redirect("/providers/connect-account");
+ }
+
+ const useCredentialsForm =
+ (searchParams.type === "aws" && searchParams.via === "credentials") ||
+ (searchParams.type !== "aws" && !searchParams.via);
+
+ const useRoleForm =
+ searchParams.type === "aws" && searchParams.via === "role";
+
+ return (
+ <>
+ {useCredentialsForm && }
+ {useRoleForm && }
+ >
+ );
+}
diff --git a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx
new file mode 100644
index 0000000000..67094101f5
--- /dev/null
+++ b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import React from "react";
+
+import { ConnectAccountForm } from "@/components/providers/workflow/forms";
+
+export default function ConnectAccountPage() {
+ return ;
+}
diff --git a/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx b/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx
new file mode 100644
index 0000000000..3e88a5ea46
--- /dev/null
+++ b/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx
@@ -0,0 +1,32 @@
+import { redirect } from "next/navigation";
+import React from "react";
+
+import { getProvider } from "@/actions/providers";
+import { LaunchScanForm } from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string };
+}
+
+export default async function LaunchScanPage({ searchParams }: Props) {
+ const providerId = searchParams.id;
+
+ if (!providerId) {
+ redirect("/providers/connect-account");
+ }
+
+ const formData = new FormData();
+ formData.append("id", providerId);
+
+ const providerData = await getProvider(formData);
+
+ const isConnected = providerData?.data?.attributes?.connection?.connected;
+
+ if (!isConnected) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+
+ );
+}
diff --git a/app/(prowler)/providers/(set-up-provider)/layout.tsx b/app/(prowler)/providers/(set-up-provider)/layout.tsx
new file mode 100644
index 0000000000..788ce9cd96
--- /dev/null
+++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx
@@ -0,0 +1,32 @@
+import "@/styles/globals.css";
+
+import { Spacer } from "@nextui-org/react";
+import React from "react";
+
+import { Workflow } from "@/components/providers/workflow";
+import { NavigationHeader } from "@/components/ui";
+
+interface ProviderLayoutProps {
+ children: React.ReactNode;
+}
+
+export default function ProviderLayout({ children }: ProviderLayoutProps) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx
new file mode 100644
index 0000000000..13cac50f6d
--- /dev/null
+++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx
@@ -0,0 +1,44 @@
+import { redirect } from "next/navigation";
+import React, { Suspense } from "react";
+
+import { getProvider } from "@/actions/providers";
+import { TestConnectionForm } from "@/components/providers/workflow/forms";
+
+interface Props {
+ searchParams: { type: string; id: string };
+}
+
+export default async function TestConnectionPage({ searchParams }: Props) {
+ const providerId = searchParams.id;
+
+ if (!providerId) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+ Loading...}>
+
+
+ );
+}
+
+async function SSRTestConnection({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) {
+ const formData = new FormData();
+ formData.append("id", searchParams.id);
+
+ const providerData = await getProvider(formData);
+ if (providerData.errors) {
+ redirect("/providers/connect-account");
+ }
+
+ return (
+
+ );
+}
diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx
index 02860e2a0b..177b403745 100644
--- a/app/(prowler)/providers/page.tsx
+++ b/app/(prowler)/providers/page.tsx
@@ -26,7 +26,6 @@ export default async function Providers({
-
diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx
index 3391207093..9483ee9b86 100644
--- a/components/findings/table/column-findings.tsx
+++ b/components/findings/table/column-findings.tsx
@@ -2,12 +2,7 @@
import { ColumnDef } from "@tanstack/react-table";
-import {
- DataTableColumnHeader,
- SeverityBadge,
- Status,
- StatusBadge,
-} from "@/components/ui/table";
+import { SeverityBadge, Status, StatusBadge } from "@/components/ui/table";
import { FindingProps } from "@/types";
import { DataTableRowActions } from "./data-table-row-actions";
@@ -20,7 +15,6 @@ const statusMap: Record<"PASS" | "FAIL" | "MANUAL" | "MUTED", Status> = {
};
const getFindingsData = (row: { original: FindingProps }) => {
- console.log(row.original);
return row.original;
};
@@ -94,7 +88,7 @@ export const ColumnFindings: ColumnDef[] = [
},
{
accessorKey: "status",
- header: "Scan Status",
+ header: "Status",
cell: ({ row }) => {
const {
attributes: { status },
diff --git a/components/findings/table/data-table-row-actions.tsx b/components/findings/table/data-table-row-actions.tsx
index 9513bb11a8..f96f804fc9 100644
--- a/components/findings/table/data-table-row-actions.tsx
+++ b/components/findings/table/data-table-row-actions.tsx
@@ -10,7 +10,6 @@ import {
} from "@nextui-org/react";
import {
// AddNoteBulkIcon,
- DeleteDocumentBulkIcon,
EditDocumentBulkIcon,
} from "@nextui-org/shared-icons";
import { Row } from "@tanstack/react-table";
@@ -31,10 +30,8 @@ const iconClasses =
export function DataTableRowActions({
row,
}: DataTableRowActionsProps) {
- // const [isEditOpen, setIsEditOpen] = useState(false);
- // const [isDeleteOpen, setIsDeleteOpen] = useState(false);
- // const providerId = (row.original as { id: string }).id;
- // const providerAlias = (row.original as any).attributes?.alias;
+ const findingId = (row.original as { id: string }).id;
+ console.log(findingId);
return (
<>
{/* = ({
);
-export const WifiIcon: React.FC = ({
- size = 24,
- width,
- height,
- ...props
-}) => (
-
-);
-
-export const WifiOffIcon: React.FC = ({
- size = 24,
- width,
- height,
- ...props
-}) => (
-
-);
-
-export const WifiPendingIcon: React.FC = ({
- size = 24,
- width,
- height,
- ...props
-}) => (
-
-);
export const DeleteIcon: React.FC = ({
size,
@@ -860,3 +758,30 @@ export const AddIcon: React.FC = ({
);
};
+
+export const ScheduleIcon: React.FC = ({
+ size,
+ height,
+ width,
+ ...props
+}) => {
+ return (
+
+ );
+};
diff --git a/components/providers/CheckConnectionProvider.tsx b/components/providers/CheckConnectionProvider.tsx
deleted file mode 100644
index d5bc9e727d..0000000000
--- a/components/providers/CheckConnectionProvider.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client";
-
-import { useRef } from "react";
-
-import { checkConnectionProvider } from "@/actions/providers";
-
-import { CustomButtonClientAction } from "../ui/custom";
-import { useToast } from "../ui/toast";
-
-export const CheckConnectionProvider = ({ id }: { id: string }) => {
- const ref = useRef(null);
- const { toast } = useToast();
-
- async function clientAction(formData: FormData) {
- // reset the form
- ref.current?.reset();
- // client-side validation
- const data = await checkConnectionProvider(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: "Checking",
- description: "The task was launched successfully",
- });
- }
- }
-
- return (
-
- );
-};
diff --git a/components/providers/add-provider.tsx b/components/providers/add-provider.tsx
index 3033f8b904..c732d41007 100644
--- a/components/providers/add-provider.tsx
+++ b/components/providers/add-provider.tsx
@@ -1,39 +1,21 @@
"use client";
-import { useState } from "react";
-
import { AddIcon } from "../icons";
-import { CustomAlertModal, CustomButton } from "../ui/custom";
-import { AddForm } from "./forms";
+import { CustomButton } from "../ui/custom";
export const AddProvider = () => {
- const [isAddOpen, setIsAddOpen] = useState(false);
-
return (
- <>
-
+ }
>
-
-
-
-
-
setIsAddOpen(true)}
- endContent={}
- >
- Add Account
-
-
- >
+ Add Account
+
+
);
};
diff --git a/components/providers/forms/add-form.tsx b/components/providers/forms/add-form.tsx
deleted file mode 100644
index 3529995cfa..0000000000
--- a/components/providers/forms/add-form.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-"use client";
-
-import { zodResolver } from "@hookform/resolvers/zod";
-import { Dispatch, SetStateAction } from "react";
-import { useForm } from "react-hook-form";
-import * as z from "zod";
-
-import { addProvider } from "@/actions/providers";
-import { SaveIcon } from "@/components/icons";
-import { useToast } from "@/components/ui";
-import { CustomButton, CustomInput } from "@/components/ui/custom";
-import { Form } from "@/components/ui/form";
-import { addProviderFormSchema } from "@/types";
-
-import { RadioGroupProvider } from "../radio-group-provider";
-
-export const AddForm = ({
- setIsOpen,
-}: {
- setIsOpen: Dispatch>;
-}) => {
- const formSchema = addProviderFormSchema;
-
- const form = useForm>({
- resolver: zodResolver(formSchema),
- defaultValues: {
- providerType: "",
- providerId: "",
- providerAlias: "",
- },
- });
-
- const { toast } = useToast();
-
- const isLoading = form.formState.isSubmitting;
-
- const onSubmitClient = async (values: z.infer) => {
- const formData = new FormData();
-
- Object.entries(values).forEach(
- ([key, value]) => value !== undefined && formData.append(key, value),
- );
-
- const data = await addProvider(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 provider was updated successfully.",
- });
- setIsOpen(false); // Close the modal on success
- }
- };
-
- return (
-
-
- );
-};
diff --git a/components/providers/forms/index.ts b/components/providers/forms/index.ts
index 6b31209f91..a081952ade 100644
--- a/components/providers/forms/index.ts
+++ b/components/providers/forms/index.ts
@@ -1,3 +1,2 @@
-export * from "./add-form";
export * from "./delete-form";
export * from "./edit-form";
diff --git a/components/providers/index.ts b/components/providers/index.ts
index 5093cbf0ac..6fb3bcae31 100644
--- a/components/providers/index.ts
+++ b/components/providers/index.ts
@@ -1,5 +1,4 @@
export * from "./add-provider";
-export * from "./CheckConnectionProvider";
export * from "./forms/delete-form";
export * from "./provider-info";
export * from "./radio-group-provider";
diff --git a/components/providers/radio-group-provider.tsx b/components/providers/radio-group-provider.tsx
index 82ee2edbae..e376e69ff4 100644
--- a/components/providers/radio-group-provider.tsx
+++ b/components/providers/radio-group-provider.tsx
@@ -1,7 +1,6 @@
"use client";
-import { UseRadioProps } from "@nextui-org/radio/dist/use-radio";
-import { cn, RadioGroup, useRadio, VisuallyHidden } from "@nextui-org/react";
+import { RadioGroup } from "@nextui-org/react";
import React from "react";
import { Control, Controller } from "react-hook-form";
import { z } from "zod";
@@ -11,61 +10,19 @@ import { addProviderFormSchema } from "@/types";
import { AWSProviderBadge, AzureProviderBadge } from "../icons/providers-badge";
import { GCPProviderBadge } from "../icons/providers-badge/GCPProviderBadge";
import { KS8ProviderBadge } from "../icons/providers-badge/KS8ProviderBadge";
+import { CustomRadio } from "../ui/custom";
import { FormMessage } from "../ui/form";
-interface CustomRadioProps extends UseRadioProps {
- description?: string;
- children?: React.ReactNode;
-}
-
-export const CustomRadio: React.FC = (props) => {
- const {
- Component,
- children,
- // description,
- getBaseProps,
- getWrapperProps,
- getInputProps,
- getLabelProps,
- getLabelWrapperProps,
- getControlProps,
- } = useRadio(props);
-
- return (
-
-
-
-
-
-
-
-
- {children && {children}}
- {/* {description && (
-
- {description}
-
- )} */}
-
-
- );
-};
-
interface RadioGroupProviderProps {
control: Control>;
isInvalid: boolean;
+ errorMessage?: string;
}
export const RadioGroupProvider: React.FC = ({
control,
isInvalid,
+ errorMessage,
}) => {
return (
= ({
<>
-
+
-
AWS
+
Amazon Web Services
- GCP
+ Google Cloud Platform
-
Azure
+
Microsoft Azure
@@ -106,7 +63,11 @@ export const RadioGroupProvider: React.FC = ({
-
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
>
)}
/>
diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx
index 35f14d395c..0642114bb4 100644
--- a/components/providers/table/data-table-row-actions.tsx
+++ b/components/providers/table/data-table-row-actions.tsx
@@ -20,7 +20,6 @@ import { useState } from "react";
import { VerticalDotsIcon } from "@/components/icons";
import { CustomAlertModal } from "@/components/ui/custom";
-import { CheckConnectionProvider } from "../CheckConnectionProvider";
import { EditForm } from "../forms";
import { DeleteForm } from "../forms/delete-form";
@@ -36,6 +35,7 @@ export function DataTableRowActions
({
const [isEditOpen, setIsEditOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const providerId = (row.original as { id: string }).id;
+ const providerType = (row.original as any).attributes?.provider;
const providerAlias = (row.original as any).attributes?.alias;
return (
<>
@@ -75,12 +75,13 @@ export function DataTableRowActions({
>
}
>
-
+ Test Connection
;
+
+export const ConnectAccountForm = () => {
+ const { toast } = useToast();
+ const [prevStep, setPrevStep] = useState(1);
+ const router = useRouter();
+
+ const formSchema = addProviderFormSchema;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerType: undefined,
+ providerUid: "",
+ providerAlias: "",
+ awsCredentialsType: "",
+ },
+ });
+ const providerType = form.watch("providerType");
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formValues = { ...values };
+
+ // If providerAlias is empty, set default value
+ if (!formValues.providerAlias.trim()) {
+ const date = new Date();
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const day = date.getDate().toString().padStart(2, "0");
+ const year = date.getFullYear();
+ formValues.providerAlias = `${formValues.providerType}:${month}/${day}/${year}`;
+ }
+
+ const formData = new FormData();
+
+ Object.entries(formValues).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await addProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/provider":
+ form.setError("providerType", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/uid":
+ case "/data/attributes/__all__":
+ form.setError("providerUid", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/alias":
+ form.setError("providerAlias", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ setPrevStep(1);
+ } else {
+ const {
+ id,
+ attributes: { provider: providerType },
+ } = data.data;
+ const credentialsParam = values.awsCredentialsType
+ ? `&via=${values.awsCredentialsType}`
+ : "";
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${id}${credentialsParam}`,
+ );
+ }
+ };
+
+ const handleNextStep = () => {
+ setPrevStep((prev) => prev + 1);
+ };
+
+ const handleBackStep = () => {
+ setPrevStep((prev) => prev - 1);
+ };
+
+ return (
+
+
+ );
+};
diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts
new file mode 100644
index 0000000000..be1bb3277d
--- /dev/null
+++ b/components/providers/workflow/forms/index.ts
@@ -0,0 +1,6 @@
+export * from "./connect-account-form";
+export * from "./launch-scan-form";
+export * from "./radio-group-aws-via-credentials-form";
+export * from "./test-connection-form";
+export * from "./via-credentials-form";
+export * from "./via-role-form";
diff --git a/components/providers/workflow/forms/launch-scan-form.tsx b/components/providers/workflow/forms/launch-scan-form.tsx
new file mode 100644
index 0000000000..86325d437b
--- /dev/null
+++ b/components/providers/workflow/forms/launch-scan-form.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { scanOnDemand } from "@/actions/scans/scans";
+import { RocketIcon, ScheduleIcon } from "@/components/icons";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { ProviderProps } from "@/types"; // Asegúrate de importar la interfaz correcta
+import { launchScanFormSchema } from "@/types/formSchemas";
+
+import { ProviderInfo } from "../../provider-info";
+
+type FormValues = z.infer>;
+
+interface LaunchScanFormProps {
+ searchParams: { type: string; id: string };
+ providerData: {
+ data: {
+ type: string;
+ id: string;
+ attributes: ProviderProps["attributes"];
+ };
+ };
+}
+
+export const LaunchScanForm = ({
+ searchParams,
+ providerData,
+}: LaunchScanFormProps) => {
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+
+ const [apiErrorMessage, setApiErrorMessage] = useState(null);
+ const router = useRouter();
+
+ const formSchema = launchScanFormSchema();
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ scannerArgs: {
+ checksToExecute: [],
+ },
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ const formData = new FormData();
+ formData.append("providerId", values.providerId);
+
+ // Generate default scan name using provider type and current date
+ const date = new Date();
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
+ const day = date.getDate().toString().padStart(2, "0");
+ const year = date.getFullYear();
+ const defaultScanName = `${providerType}:${month}/${day}/${year}`;
+
+ formData.append("scanName", defaultScanName);
+
+ try {
+ const data = await scanOnDemand(formData);
+
+ if (data.error) {
+ setApiErrorMessage(data.error);
+ form.setError("providerId", {
+ type: "server",
+ message: data.error,
+ });
+ } else {
+ router.push("/scans");
+ }
+ } catch (error) {
+ form.setError("providerId", {
+ type: "server",
+ message: "An unexpected error occurred. Please try again.",
+ });
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx b/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx
new file mode 100644
index 0000000000..7a870c79ff
--- /dev/null
+++ b/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { RadioGroup } from "@nextui-org/react";
+import React from "react";
+import { Control, Controller } from "react-hook-form";
+
+import { CustomRadio } from "@/components/ui/custom";
+import { FormMessage } from "@/components/ui/form";
+
+import { FormValues } from "./connect-account-form";
+
+type RadioGroupAWSViaCredentialsFormProps = {
+ control: Control;
+ isInvalid: boolean;
+ errorMessage?: string;
+};
+
+export const RadioGroupAWSViaCredentialsForm = ({
+ control,
+ isInvalid,
+ errorMessage,
+}: RadioGroupAWSViaCredentialsFormProps) => {
+ return (
+ (
+ <>
+
+
+
Using IAM Role
+
+
+ Connect assuming IAM Role
+
+
+
+ Using Credentials
+
+
+
+ Connect via Credentials
+
+
+
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
+ >
+ )}
+ />
+ );
+};
diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx
new file mode 100644
index 0000000000..6d7ca16dbe
--- /dev/null
+++ b/components/providers/workflow/forms/test-connection-form.tsx
@@ -0,0 +1,268 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Icon } from "@iconify/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import {
+ checkConnectionProvider,
+ deleteCredentials,
+} from "@/actions/providers";
+import { getTask } from "@/actions/task/tasks";
+import { CheckIcon, SaveIcon } from "@/components/icons";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import { checkTaskStatus } from "@/lib/helper";
+import { ApiError, testConnectionFormSchema } from "@/types";
+
+import { ProviderInfo } from "../..";
+
+type FormValues = z.infer;
+
+export const TestConnectionForm = ({
+ searchParams,
+ providerData,
+}: {
+ searchParams: { type: string; id: string };
+ providerData: {
+ data: {
+ id: string;
+ type: string;
+ attributes: {
+ connection: {
+ connected: boolean | null;
+ last_checked_at: string | null;
+ };
+ provider: "aws" | "azure" | "gcp" | "kubernetes";
+ alias: string;
+ scanner_args: Record;
+ };
+ relationships: {
+ secret: {
+ data: {
+ type: string;
+ id: string;
+ } | null;
+ };
+ };
+ };
+ };
+}) => {
+ const { toast } = useToast();
+ const router = useRouter();
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+ console.log({ providerData }, "providerData from test connection form");
+ const formSchema = testConnectionFormSchema;
+ const [apiErrorMessage, setApiErrorMessage] = useState(null);
+ const [connectionStatus, setConnectionStatus] = useState<{
+ connected: boolean;
+ error: string | null;
+ } | null>(null);
+ const [isResettingCredentials, setIsResettingCredentials] = useState(false);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormValues) => {
+ console.log({ values }, "values from test connection form");
+ const formData = new FormData();
+ formData.append("providerId", values.providerId);
+
+ const data = await checkConnectionProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+
+ switch (errorMessage) {
+ case "Not found.":
+ setApiErrorMessage(errorMessage);
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: `Error ${error.status}`,
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ const taskId = data.data.id;
+ setApiErrorMessage(null);
+
+ // Use the helper function to check the task status
+ const taskResult = await checkTaskStatus(taskId);
+
+ if (taskResult.completed) {
+ const task = await getTask(taskId);
+ const { connected, error } = task.data.attributes.result;
+
+ setConnectionStatus({
+ connected,
+ error: connected ? null : error || "Unknown error",
+ });
+
+ if (connected) {
+ router.push(
+ `/providers/launch-scan?type=${providerType}&id=${providerId}`,
+ );
+ } else {
+ setConnectionStatus({
+ connected: false,
+ error: error || "Connection failed, please review credentials.",
+ });
+ }
+ } else {
+ setConnectionStatus({
+ connected: false,
+ error: taskResult.error || "Unknown error",
+ });
+ }
+ }
+ };
+
+ const onResetCredentials = async () => {
+ setIsResettingCredentials(true);
+
+ // Check if provider the provider has no credentials
+ const providerSecretId =
+ providerData?.data?.relationships?.secret?.data?.id;
+ const hasNoCredentials = !providerSecretId;
+
+ if (hasNoCredentials) {
+ // If no credentials, redirect to add credentials page
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${providerId}`,
+ );
+ return;
+ }
+
+ // If provider has credentials, delete them first
+ try {
+ await deleteCredentials(providerSecretId);
+ // After successful deletion, redirect to add credentials page
+ router.push(
+ `/providers/add-credentials?type=${providerType}&id=${providerId}`,
+ );
+ } catch (error) {
+ console.error("Failed to delete credentials:", error);
+ } finally {
+ setIsResettingCredentials(false);
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx
new file mode 100644
index 0000000000..176ed40a00
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials-form.tsx
@@ -0,0 +1,207 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Control, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { addCredentialsProvider } from "@/actions/providers/providers";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import {
+ addCredentialsFormSchema,
+ ApiError,
+ AWSCredentials,
+ AzureCredentials,
+ GCPCredentials,
+ KubernetesCredentials,
+} from "@/types";
+
+import { AWScredentialsForm } from "./via-credentials/aws-credentials-form";
+import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
+import { GCPcredentialsForm } from "./via-credentials/gcp-credentials-form";
+import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
+
+type CredentialsFormSchema = z.infer<
+ ReturnType
+>;
+
+// Add this type intersection to include all fields
+type FormType = CredentialsFormSchema &
+ AWSCredentials &
+ AzureCredentials &
+ GCPCredentials &
+ KubernetesCredentials;
+
+export const ViaCredentialsForm = ({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+ const formSchema = addCredentialsFormSchema(providerType);
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ ...(providerType === "aws"
+ ? {
+ aws_access_key_id: "",
+ aws_secret_access_key: "",
+ aws_session_token: "",
+ }
+ : providerType === "azure"
+ ? {
+ client_id: "",
+ client_secret: "",
+ tenant_id: "",
+ }
+ : providerType === "gcp"
+ ? {
+ client_id: "",
+ client_secret: "",
+ refresh_token: "",
+ }
+ : providerType === "kubernetes"
+ ? {
+ kubeconfig_content: "",
+ }
+ : {}),
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormType) => {
+ console.log("via credentials form", values);
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) => value !== undefined && formData.append(key, value),
+ );
+
+ const data = await addCredentialsProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/secret/aws_access_key_id":
+ form.setError("aws_access_key_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/aws_secret_access_key":
+ form.setError("aws_secret_access_key", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/aws_session_token":
+ form.setError("aws_session_token", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/client_id":
+ form.setError("client_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/client_secret":
+ form.setError("client_secret", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/tenant_id":
+ form.setError("tenant_id", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/secret/kubeconfig_content":
+ form.setError("kubeconfig_content", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ case "/data/attributes/name":
+ form.setError("secretName", {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ router.push(
+ `/providers/test-connection?type=${providerType}&id=${providerId}`,
+ );
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx
new file mode 100644
index 0000000000..a024d138ff
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AWSCredentials } from "@/types";
+
+export const AWScredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your AWS credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
new file mode 100644
index 0000000000..b78642b8d2
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AzureCredentials } from "@/types";
+
+export const AzureCredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your Azure credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx
new file mode 100644
index 0000000000..784f173f49
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx
@@ -0,0 +1,56 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { GCPCredentials } from "@/types";
+
+export const GCPcredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your GCP credentials.
+
+
+
+
+
+ >
+ );
+};
diff --git a/components/providers/workflow/forms/via-credentials/index.ts b/components/providers/workflow/forms/via-credentials/index.ts
new file mode 100644
index 0000000000..d3198a1d0e
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials/index.ts
@@ -0,0 +1,4 @@
+export * from "./aws-credentials-form";
+export * from "./azure-credentials-form";
+export * from "./gcp-credentials-form";
+export * from "./k8s-credentials-form";
diff --git a/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
new file mode 100644
index 0000000000..0725d134eb
--- /dev/null
+++ b/components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx
@@ -0,0 +1,34 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { KubernetesCredentials } from "@/types";
+
+export const KubernetesCredentialsForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect via Credentials
+
+
+ Please provide the information for your Kubernetes credentials.
+
+
+
+ >
+ );
+};
diff --git a/components/providers/workflow/forms/via-role-form.tsx b/components/providers/workflow/forms/via-role-form.tsx
new file mode 100644
index 0000000000..39ba301447
--- /dev/null
+++ b/components/providers/workflow/forms/via-role-form.tsx
@@ -0,0 +1,125 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { SaveIcon } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { Control, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { addCredentialsProvider } from "@/actions/providers/providers";
+import { useToast } from "@/components/ui";
+import { CustomButton } from "@/components/ui/custom";
+import { Form } from "@/components/ui/form";
+import {
+ addCredentialsRoleFormSchema,
+ ApiError,
+ AWSCredentialsRole,
+} from "@/types";
+
+import { AWSCredentialsRoleForm } from "./via-role/aws-role-form";
+
+export const ViaRoleForm = ({
+ searchParams,
+}: {
+ searchParams: { type: string; id: string };
+}) => {
+ const router = useRouter();
+ const { toast } = useToast();
+
+ const providerType = searchParams.type;
+ const providerId = searchParams.id;
+
+ const formSchema = addCredentialsRoleFormSchema(providerType);
+ type FormSchemaType = z.infer;
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ providerId,
+ providerType,
+ ...(providerType === "aws"
+ ? {
+ role_arn: "",
+ aws_access_key_id: "",
+ aws_secret_access_key: "",
+ aws_session_token: "",
+ session_duration: 3600,
+ external_id: "",
+ role_session_name: "",
+ }
+ : {}),
+ },
+ });
+
+ const isLoading = form.formState.isSubmitting;
+
+ const onSubmitClient = async (values: FormSchemaType) => {
+ console.log("via ROLE form", values);
+ const formData = new FormData();
+
+ Object.entries(values).forEach(
+ ([key, value]) =>
+ value !== undefined && formData.append(key, String(value)),
+ );
+
+ const data = await addCredentialsProvider(formData);
+
+ if (data?.errors && data.errors.length > 0) {
+ data.errors.forEach((error: ApiError) => {
+ const errorMessage = error.detail;
+ switch (error.source.pointer) {
+ case "/data/attributes/secret/role_arn":
+ form.setError("role_arn" as keyof FormSchemaType, {
+ type: "server",
+ message: errorMessage,
+ });
+ break;
+
+ default:
+ toast({
+ variant: "destructive",
+ title: "Oops! Something went wrong",
+ description: errorMessage,
+ });
+ }
+ });
+ } else {
+ router.push(
+ `/providers/test-connection?type=${providerType}&id=${providerId}`,
+ );
+ }
+ };
+
+ return (
+
+
+ );
+};
diff --git a/components/providers/workflow/forms/via-role/aws-role-form.tsx b/components/providers/workflow/forms/via-role/aws-role-form.tsx
new file mode 100644
index 0000000000..eb7f5d3a43
--- /dev/null
+++ b/components/providers/workflow/forms/via-role/aws-role-form.tsx
@@ -0,0 +1,105 @@
+import { Control } from "react-hook-form";
+
+import { CustomInput } from "@/components/ui/custom";
+import { AWSCredentialsRole } from "@/types";
+
+export const AWSCredentialsRoleForm = ({
+ control,
+}: {
+ control: Control;
+}) => {
+ return (
+ <>
+
+
+ Connect assuming IAM Role
+
+
+ Please provide the information for your AWS credentials.
+
+
+
+
+ Optional fields
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/components/providers/workflow/forms/via-role/index.ts b/components/providers/workflow/forms/via-role/index.ts
new file mode 100644
index 0000000000..763c90c0c0
--- /dev/null
+++ b/components/providers/workflow/forms/via-role/index.ts
@@ -0,0 +1 @@
+export * from "./aws-role-form";
diff --git a/components/providers/workflow/index.ts b/components/providers/workflow/index.ts
new file mode 100644
index 0000000000..f245196eab
--- /dev/null
+++ b/components/providers/workflow/index.ts
@@ -0,0 +1,2 @@
+export * from "./vertical-steps";
+export * from "./workflow";
diff --git a/components/providers/workflow/vertical-steps.tsx b/components/providers/workflow/vertical-steps.tsx
new file mode 100644
index 0000000000..74eaa32747
--- /dev/null
+++ b/components/providers/workflow/vertical-steps.tsx
@@ -0,0 +1,291 @@
+"use client";
+
+import type { ButtonProps } from "@nextui-org/react";
+import { cn } from "@nextui-org/react";
+import { useControlledState } from "@react-stately/utils";
+import { domAnimation, LazyMotion, m } from "framer-motion";
+import type { ComponentProps } from "react";
+import React from "react";
+
+export type VerticalStepProps = {
+ className?: string;
+ description?: React.ReactNode;
+ title?: React.ReactNode;
+};
+
+export interface VerticalStepsProps
+ extends React.HTMLAttributes {
+ /**
+ * An array of steps.
+ *
+ * @default []
+ */
+ steps?: VerticalStepProps[];
+ /**
+ * The color of the steps.
+ *
+ * @default "primary"
+ */
+ color?: ButtonProps["color"];
+ /**
+ * The current step index.
+ */
+ currentStep?: number;
+ /**
+ * The default step index.
+ *
+ * @default 0
+ */
+ defaultStep?: number;
+ /**
+ * Whether to hide the progress bars.
+ *
+ * @default false
+ */
+ hideProgressBars?: boolean;
+ /**
+ * The custom class for the steps wrapper.
+ */
+ className?: string;
+ /**
+ * The custom class for the step.
+ */
+ stepClassName?: string;
+ /**
+ * Callback function when the step index changes.
+ */
+ onStepChange?: (stepIndex: number) => void;
+}
+
+function CheckIcon(props: ComponentProps<"svg">) {
+ return (
+
+ );
+}
+
+export const VerticalSteps = React.forwardRef<
+ HTMLButtonElement,
+ VerticalStepsProps
+>(
+ (
+ {
+ color = "primary",
+ steps = [],
+ defaultStep = 0,
+ onStepChange,
+ currentStep: currentStepProp,
+ hideProgressBars = false,
+ stepClassName,
+ className,
+ ...props
+ },
+ ref,
+ ) => {
+ const [currentStep, setCurrentStep] = useControlledState(
+ currentStepProp,
+ defaultStep,
+ onStepChange,
+ );
+
+ const colors = React.useMemo(() => {
+ let userColor;
+ let fgColor;
+
+ const colorsVars = [
+ "[--active-fg-color:var(--step-fg-color)]",
+ "[--active-border-color:var(--step-color)]",
+ "[--active-color:var(--step-color)]",
+ "[--complete-background-color:var(--step-color)]",
+ "[--complete-border-color:var(--step-color)]",
+ "[--inactive-border-color:hsl(var(--nextui-default-300))]",
+ "[--inactive-color:hsl(var(--nextui-default-300))]",
+ ];
+
+ switch (color) {
+ case "primary":
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ case "secondary":
+ userColor = "[--step-color:hsl(var(--nextui-secondary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-secondary-foreground))]";
+ break;
+ case "success":
+ userColor = "[--step-color:hsl(var(--nextui-success))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-success-foreground))]";
+ break;
+ case "warning":
+ userColor = "[--step-color:hsl(var(--nextui-warning))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-warning-foreground))]";
+ break;
+ case "danger":
+ userColor = "[--step-color:hsl(var(--nextui-error))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-error-foreground))]";
+ break;
+ case "default":
+ userColor = "[--step-color:hsl(var(--nextui-default))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-default-foreground))]";
+ break;
+ default:
+ userColor = "[--step-color:hsl(var(--nextui-primary))]";
+ fgColor = "[--step-fg-color:hsl(var(--nextui-primary-foreground))]";
+ break;
+ }
+
+ if (!className?.includes("--step-fg-color")) colorsVars.unshift(fgColor);
+ if (!className?.includes("--step-color")) colorsVars.unshift(userColor);
+ if (!className?.includes("--inactive-bar-color"))
+ colorsVars.push(
+ "[--inactive-bar-color:hsl(var(--nextui-default-300))]",
+ );
+
+ return colorsVars;
+ }, [color, className]);
+
+ return (
+
+ );
+ },
+);
+
+VerticalSteps.displayName = "VerticalSteps";
diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx
new file mode 100644
index 0000000000..da5fa18f99
--- /dev/null
+++ b/components/providers/workflow/workflow.tsx
@@ -0,0 +1,76 @@
+"use client";
+
+import { Progress, Spacer } from "@nextui-org/react";
+import { usePathname } from "next/navigation";
+import React from "react";
+
+import { VerticalSteps } from "./vertical-steps";
+
+const steps = [
+ {
+ title: "Add your cloud account",
+ description:
+ "Select the cloud provider of the account you want to connect and choose whether to use IAM role or credentials for access.",
+ href: "/providers/connect-account",
+ },
+ {
+ title: "Add credentials to your cloud account",
+ description: "Add the credentials needed to connect to your cloud account.",
+ href: "/providers/add-credentials",
+ },
+ {
+ title: "Test connection",
+ description:
+ "Test your connection to verify that the credentials provided are valid for accessing your cloud account.",
+ href: "/providers/test-connection",
+ },
+ {
+ title: "Launch scan",
+ description:
+ "Launch the scan now or schedule it for a later date and time.",
+ href: "/providers/launch-scan",
+ },
+];
+
+export const Workflow = () => {
+ const pathname = usePathname();
+
+ // Calculate current step based on pathname
+ const currentStepIndex = steps.findIndex((step) =>
+ pathname.endsWith(step.href),
+ );
+ const currentStep = currentStepIndex === -1 ? 0 : currentStepIndex;
+
+ return (
+
+
+ Add a cloud account
+
+
+ Follow the steps to configure your cloud account. This allows you to
+ launch the first scan when the process is complete.
+
+
+
+
+
+ );
+};
diff --git a/components/ui/custom/custom-button.tsx b/components/ui/custom/custom-button.tsx
index b2a6cf355a..eeffd07783 100644
--- a/components/ui/custom/custom-button.tsx
+++ b/components/ui/custom/custom-button.tsx
@@ -1,6 +1,7 @@
import { Button, CircularProgress } from "@nextui-org/react";
import type { PressEvent } from "@react-types/shared";
import clsx from "clsx";
+import Link from "next/link";
import React from "react";
import { NextUIColors, NextUIVariants } from "@/types";
@@ -50,6 +51,7 @@ interface CustomButtonProps {
isLoading?: boolean;
isIconOnly?: boolean;
ref?: React.RefObject;
+ asLink?: string;
}
export const CustomButton = React.forwardRef<
@@ -73,11 +75,14 @@ export const CustomButton = React.forwardRef<
isDisabled = false,
isLoading = false,
isIconOnly,
+ asLink,
...props
},
ref,
) => (
diff --git a/lib/helper.ts b/lib/helper.ts
index 7eeee17a6f..aed48d9478 100644
--- a/lib/helper.ts
+++ b/lib/helper.ts
@@ -1,17 +1,61 @@
+import { getTask } from "@/actions/task";
import { MetaDataProps } from "@/types";
+export async function checkTaskStatus(
+ taskId: string,
+): Promise<{ completed: boolean; error?: string }> {
+ const MAX_RETRIES = 20; // Define the maximum number of attempts before stopping the polling
+ const RETRY_DELAY = 1000; // Delay time between each poll (in milliseconds)
+
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ const task = await getTask(taskId);
+
+ if (task.error) {
+ // eslint-disable-next-line no-console
+ 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, RETRY_DELAY));
+ break;
+ default:
+ console.warn(`Unexpected task state: ${state}`);
+ 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));
+
// Helper function to create dictionaries by type
-export const createDict = (
- type: string,
- data: any,
- includedField: string = "included",
-) =>
- Object.fromEntries(
- data[includedField]
- .filter((item: { type: string }) => item.type === type)
- .map((item: { id: string }) => [item.id, item]),
+export function createDict(type: string, data: any) {
+ const includedField = data?.included?.filter(
+ (item: { type: string }) => item.type === type,
);
+ if (!includedField || includedField.length === 0) {
+ return {};
+ }
+
+ return Object.fromEntries(
+ includedField.map((item: { id: string }) => [item.id, item]),
+ );
+}
+
export const parseStringify = (value: any) => JSON.parse(JSON.stringify(value));
export const convertFileToUrl = (file: File) => URL.createObjectURL(file);
diff --git a/tailwind.config.js b/tailwind.config.js
index 8efddab52b..9d06ef139a 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -23,7 +23,7 @@ module.exports = {
theme: {
midnight: "#030921",
pale: "#f3fcff",
- green: "#6af400",
+ green: "#8ce112",
purple: "#5001d0",
coral: "#ff5356",
orange: "#f69000",
@@ -83,7 +83,7 @@ module.exports = {
},
},
danger: "#E11D48",
- action: "#6af400",
+ action: "#9FD655",
},
fontFamily: {
sans: ["var(--font-sans)"],
@@ -177,13 +177,21 @@ module.exports = {
dark: {
colors: {
primary: {
- DEFAULT: "#6af400",
+ DEFAULT: "#9FD655",
foreground: "#000000",
},
- focus: "#6af400",
+ focus: "#9FD655",
background: "#030921",
},
},
+ light: {
+ colors: {
+ primary: {
+ DEFAULT: "#9FD655",
+ foreground: "#000000",
+ },
+ },
+ },
},
}),
],
diff --git a/types/components.ts b/types/components.ts
index 086e6e4a60..b6c4b59da2 100644
--- a/types/components.ts
+++ b/types/components.ts
@@ -26,6 +26,52 @@ export type NextUIColors =
| "danger"
| "default";
+export type AWSCredentials = {
+ aws_access_key_id: string;
+ aws_secret_access_key: string;
+ aws_session_token: string;
+ secretName: string;
+ providerId: string;
+};
+
+export type AWSCredentialsRole = {
+ role_arn: string;
+ aws_access_key_id?: string;
+ aws_secret_access_key?: string;
+ aws_session_token?: string;
+ external_id?: string;
+ role_session_name?: string;
+ session_duration?: number;
+};
+
+export type AzureCredentials = {
+ client_id: string;
+ client_secret: string;
+ tenant_id: string;
+ secretName: string;
+ providerId: string;
+};
+
+export type GCPCredentials = {
+ client_id: string;
+ client_secret: string;
+ refresh_token: string;
+ secretName: string;
+ providerId: string;
+};
+
+export type KubernetesCredentials = {
+ kubeconfig_content: string;
+ secretName: string;
+ providerId: string;
+};
+
+export type CredentialsFormSchema =
+ | AWSCredentials
+ | AzureCredentials
+ | GCPCredentials
+ | KubernetesCredentials;
+
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;
}
diff --git a/types/formSchemas.ts b/types/formSchemas.ts
index e1df1285b8..a90ffc8c02 100644
--- a/types/formSchemas.ts
+++ b/types/formSchemas.ts
@@ -31,12 +31,112 @@ export const scheduleScanFormSchema = () =>
scheduleDate: z.string(),
});
-export const addProviderFormSchema = z.object({
- providerType: z.string(),
- providerAlias: z.string(),
+export const addProviderFormSchema = z
+ .object({
+ providerType: z.enum(["aws", "azure", "gcp", "kubernetes"], {
+ required_error: "Please select a provider type",
+ }),
+ })
+ .and(
+ z.discriminatedUnion("providerType", [
+ z.object({
+ providerType: z.literal("aws"),
+ providerAlias: z.string(),
+ providerUid: z.string(),
+ awsCredentialsType: z.string().min(1, {
+ message: "Please select the type of credentials you want to use",
+ }),
+ }),
+ z.object({
+ providerType: z.literal("azure"),
+ providerAlias: z.string(),
+ providerUid: z.string(),
+ awsCredentialsType: z.string().optional(),
+ }),
+ z.object({
+ providerType: z.literal("gcp"),
+ providerAlias: z.string(),
+ providerUid: z.string(),
+ awsCredentialsType: z.string().optional(),
+ }),
+ z.object({
+ providerType: z.literal("kubernetes"),
+ providerAlias: z.string(),
+ providerUid: z.string(),
+ awsCredentialsType: z.string().optional(),
+ }),
+ ]),
+ );
+
+export const addCredentialsFormSchema = (providerType: string) =>
+ z.object({
+ secretName: z.string().optional(),
+ providerId: z.string(),
+ providerType: z.string(),
+ ...(providerType === "aws"
+ ? {
+ aws_access_key_id: z
+ .string()
+ .nonempty("AWS Access Key ID is required"),
+ aws_secret_access_key: z
+ .string()
+ .nonempty("AWS Secret Access Key is required"),
+ aws_session_token: z.string().optional(),
+ }
+ : providerType === "azure"
+ ? {
+ client_id: z.string().nonempty("Client ID is required"),
+ client_secret: z.string().nonempty("Client Secret is required"),
+ tenant_id: z.string().nonempty("Tenant ID is required"),
+ }
+ : providerType === "gcp"
+ ? {
+ client_id: z.string().nonempty("Client ID is required"),
+ client_secret: z.string().nonempty("Client Secret is required"),
+ refresh_token: z.string().nonempty("Refresh Token is required"),
+ }
+ : providerType === "kubernetes"
+ ? {
+ kubeconfig_content: z
+ .string()
+ .nonempty("Kubeconfig Content is required"),
+ }
+ : {}),
+ });
+
+export const addCredentialsRoleFormSchema = (providerType: string) =>
+ providerType === "aws"
+ ? z.object({
+ providerId: z.string(),
+ providerType: z.string(),
+ role_arn: z.string().optional(),
+ aws_access_key_id: z.string().optional(),
+ aws_secret_access_key: z.string().optional(),
+ aws_session_token: z.string().optional(),
+ session_duration: z.number().optional(),
+ external_id: z.string().optional(),
+ role_session_name: z.string().optional(),
+ })
+ : z.object({
+ providerId: z.string(),
+ providerType: z.string(),
+ });
+
+export const testConnectionFormSchema = z.object({
providerId: z.string(),
});
+export const launchScanFormSchema = () =>
+ z.object({
+ providerId: z.string(),
+ providerType: z.string(),
+ scannerArgs: z
+ .object({
+ checksToExecute: z.array(z.string()).optional(),
+ })
+ .optional(),
+ });
+
export const editProviderFormSchema = (currentAlias: string) =>
z.object({
alias: z