From 753c1283577180043a25b08b1984f2ad16ef00df Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 28 Oct 2024 07:45:07 +0100 Subject: [PATCH 01/44] chore: remove unused console log --- components/findings/table/column-findings.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index 3391207093..5ccfee3d23 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; }; From 52526800f9fbd69a7531e83c8e102b4380eaf7c7 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 29 Oct 2024 09:52:03 +0100 Subject: [PATCH 02/44] feat: add new component - navigation header --- .../{header/Header.tsx => headers/header.tsx} | 2 +- components/ui/headers/navigation-header.tsx | 34 +++++++++++++++++++ components/ui/index.ts | 3 +- 3 files changed, 37 insertions(+), 2 deletions(-) rename components/ui/{header/Header.tsx => headers/header.tsx} (86%) create mode 100644 components/ui/headers/navigation-header.tsx diff --git a/components/ui/header/Header.tsx b/components/ui/headers/header.tsx similarity index 86% rename from components/ui/header/Header.tsx rename to components/ui/headers/header.tsx index b7ccefaf7b..8bff672896 100644 --- a/components/ui/header/Header.tsx +++ b/components/ui/headers/header.tsx @@ -12,7 +12,7 @@ export const Header: React.FC = ({ title, icon }) => { <>
-

{title}

+

{title}

diff --git a/components/ui/headers/navigation-header.tsx b/components/ui/headers/navigation-header.tsx new file mode 100644 index 0000000000..53ffa74801 --- /dev/null +++ b/components/ui/headers/navigation-header.tsx @@ -0,0 +1,34 @@ +import { Icon } from "@iconify/react"; +import { Divider } from "@nextui-org/react"; +import Link from "next/link"; +import React from "react"; + +interface NavigationHeaderProps { + title: string; + icon: string; + href?: string; +} + +export const NavigationHeader: React.FC = ({ + title, + icon, + href, +}) => { + return ( + <> +
+ + + + +

{title}

+
+ + ); +}; diff --git a/components/ui/index.ts b/components/ui/index.ts index 07631d9636..a6ce5369af 100644 --- a/components/ui/index.ts +++ b/components/ui/index.ts @@ -4,7 +4,8 @@ export * from "./alert-dialog/AlertDialog"; export * from "./chart/Chart"; export * from "./dialog/dialog"; export * from "./dropdown/Dropdown"; -export * from "./header/Header"; +export * from "./headers/header"; +export * from "./headers/navigation-header"; export * from "./label/Label"; export * from "./select/Select"; export * from "./sidebar"; From 0a801d29cdd68244ff977df5c4e97ab120b74518 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 29 Oct 2024 10:01:46 +0100 Subject: [PATCH 03/44] feat: add new component - navigation header --- components/ui/headers/navigation-header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ui/headers/navigation-header.tsx b/components/ui/headers/navigation-header.tsx index 53ffa74801..3b7fc71a9b 100644 --- a/components/ui/headers/navigation-header.tsx +++ b/components/ui/headers/navigation-header.tsx @@ -18,7 +18,7 @@ export const NavigationHeader: React.FC = ({ <>
Date: Tue, 29 Oct 2024 12:22:03 +0100 Subject: [PATCH 04/44] feat: add new component - workflow to set up providers --- components/providers/workflow/index.ts | 2 + .../providers/workflow/vertical-steps.tsx | 291 ++++++++++++++++++ components/providers/workflow/workflow.tsx | 73 +++++ 3 files changed, 366 insertions(+) create mode 100644 components/providers/workflow/index.ts create mode 100644 components/providers/workflow/vertical-steps.tsx create mode 100644 components/providers/workflow/workflow.tsx 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..2abc3864dc --- /dev/null +++ b/components/providers/workflow/workflow.tsx @@ -0,0 +1,73 @@ +"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: "Please add your cloud account to get started.", + href: "/providers/connect-account", + }, + { + title: "Add credentials to your cloud provider", + description: "Please add your credentials to your cloud provider.", + href: "/providers/add-credentials", + }, + { + title: "Test connection", + description: "Please test your connection to your cloud provider.", + href: "/providers/test-connection", + }, + { + title: "Lunch scan", + description: "Please choose when you want to launch your scan.", + href: "/providers", + }, +]; + +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 ( +
+

+ Getting started +

+

+ Follow the steps to configure your cloud account. This allows you to + launch the first scan when the process is complete. +

+ + + +
+ ); +}; From 5a8d6087f913c5b7aea2fcdcd1044425ad7f4e8c Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 29 Oct 2024 16:00:09 +0100 Subject: [PATCH 05/44] wIP --- .../add-credentials/page.tsx | 15 ++ .../connect-account/page.tsx | 19 +++ .../providers/(set-up-provider)/layout.tsx | 22 +++ .../test-connection/page.tsx | 5 + app/(prowler)/providers/page.tsx | 3 +- .../workflow/forms/connect-account-form.tsx | 136 ++++++++++++++++++ components/providers/workflow/forms/index.ts | 1 + 7 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx create mode 100644 app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx create mode 100644 app/(prowler)/providers/(set-up-provider)/layout.tsx create mode 100644 app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx create mode 100644 components/providers/workflow/forms/connect-account-form.tsx create mode 100644 components/providers/workflow/forms/index.ts 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..1b9724578b --- /dev/null +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -0,0 +1,15 @@ +import React from "react"; + +import { NavigationHeader } from "@/components/ui"; + +export default function AddCredentialsPage() { + return ( + <> + + + ); +} 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..4c90992e4b --- /dev/null +++ b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import React from "react"; + +import { ConnectAccountForm } from "@/components/providers/workflow/forms"; +import { NavigationHeader } from "@/components/ui"; + +export default function ConnectAccountPage() { + 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..ffdc28d67d --- /dev/null +++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx @@ -0,0 +1,22 @@ +import "@/styles/globals.css"; + +import React from "react"; + +import { Workflow } from "@/components/providers/workflow"; + +interface ProviderLayoutProps { + children: React.ReactNode; +} + +export default function ProviderLayout({ children }: ProviderLayoutProps) { + return ( + <> +
+
+ +
+
{children}
+
+ + ); +} 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..8138766f63 --- /dev/null +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function TestConnectionPage() { + return
TestConnectionPage
; +} diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index 02860e2a0b..f80b59241e 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -1,4 +1,5 @@ import { Spacer } from "@nextui-org/react"; +import Link from "next/link"; import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; @@ -26,7 +27,7 @@ export default async function Providers({ - + add provider diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx new file mode 100644 index 0000000000..9de6342c1b --- /dev/null +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; + +import { addProvider } from "../../../../actions/providers/providers"; +import { addProviderFormSchema, ApiError } from "../../../../types"; +import { RadioGroupProvider } from "../../radio-group-provider"; + +export const ConnectAccountForm = () => { + 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) { + 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": + form.setError("providerId", { + 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, + }); + } + }); + } + }; + + return ( +
+ + + + + +
+ + Cancel + + + } + > + {isLoading ? <>Loading : Next} + +
+ + + ); +}; diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts new file mode 100644 index 0000000000..0bfa7550e7 --- /dev/null +++ b/components/providers/workflow/forms/index.ts @@ -0,0 +1 @@ +export * from "./connect-account-form"; From e468a91468374b26963fefb30695275ce8fa4e6f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 03:47:07 +0100 Subject: [PATCH 06/44] WIP --- .../providers/(set-up-provider)/connect-account/page.tsx | 6 ------ app/(prowler)/providers/(set-up-provider)/layout.tsx | 8 ++++++++ components/providers/radio-group-provider.tsx | 1 - .../providers/workflow/forms/connect-account-form.tsx | 5 +++-- components/providers/workflow/workflow.tsx | 5 +++-- tailwind.config.js | 2 +- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx index 4c90992e4b..1b4f1bf0f0 100644 --- a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx @@ -3,16 +3,10 @@ import React from "react"; import { ConnectAccountForm } from "@/components/providers/workflow/forms"; -import { NavigationHeader } from "@/components/ui"; export default function ConnectAccountPage() { return ( <> - ); diff --git a/app/(prowler)/providers/(set-up-provider)/layout.tsx b/app/(prowler)/providers/(set-up-provider)/layout.tsx index ffdc28d67d..59aa669510 100644 --- a/app/(prowler)/providers/(set-up-provider)/layout.tsx +++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx @@ -1,8 +1,10 @@ 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; @@ -11,6 +13,12 @@ interface ProviderLayoutProps { export default function ProviderLayout({ children }: ProviderLayoutProps) { return ( <> + +
diff --git a/components/providers/radio-group-provider.tsx b/components/providers/radio-group-provider.tsx index 82ee2edbae..6ca3de5263 100644 --- a/components/providers/radio-group-provider.tsx +++ b/components/providers/radio-group-provider.tsx @@ -75,7 +75,6 @@ export const RadioGroupProvider: React.FC = ({ <> diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 9de6342c1b..71d90ffaaa 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -49,6 +49,7 @@ export const ConnectAccountForm = () => { }); break; case "/data/attributes/uid": + case "/data/attributes/__all__": form.setError("providerId", { type: "server", message: errorMessage, @@ -85,9 +86,9 @@ export const ConnectAccountForm = () => { control={form.control} name="providerId" type="text" - label="Provider ID" + label="Provider UID" labelPlacement="inside" - placeholder={"Enter the provider ID"} + placeholder={"Enter the provider UID"} variant="bordered" isRequired isInvalid={!!form.formState.errors.providerId} diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx index 2abc3864dc..20555b3574 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow.tsx @@ -9,7 +9,8 @@ import { VerticalSteps } from "./vertical-steps"; const steps = [ { title: "Add your cloud account", - description: "Please add your cloud account to get started.", + description: + "Select the cloud provider of the account you want to connect.", href: "/providers/connect-account", }, { @@ -41,7 +42,7 @@ export const Workflow = () => { return (

- Getting started + Add a cloud account

Follow the steps to configure your cloud account. This allows you to diff --git a/tailwind.config.js b/tailwind.config.js index 8efddab52b..ce91ea47b0 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", From ccc80d5ce47acf4fcfe550be3dfba9e4db063eb8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 04:03:51 +0100 Subject: [PATCH 07/44] WIP --- .../providers/(set-up-provider)/layout.tsx | 4 ++-- .../workflow/forms/connect-account-form.tsx | 2 +- tailwind.config.js | 14 +++++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/layout.tsx b/app/(prowler)/providers/(set-up-provider)/layout.tsx index 59aa669510..b57ca47ef9 100644 --- a/app/(prowler)/providers/(set-up-provider)/layout.tsx +++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx @@ -20,10 +20,10 @@ export default function ProviderLayout({ children }: ProviderLayoutProps) { />

-
+
-
{children}
+
{children}
); diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 71d90ffaaa..de5f23c45a 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -97,7 +97,7 @@ export const ConnectAccountForm = () => { control={form.control} name="providerAlias" type="text" - label="Alias" + label="Alias (optional)" labelPlacement="inside" placeholder={"Enter the provider alias"} variant="bordered" diff --git a/tailwind.config.js b/tailwind.config.js index ce91ea47b0..9d06ef139a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -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", + }, + }, + }, }, }), ], From 886e3aefb046442862a6f2a209765f7718787fa7 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 04:09:19 +0100 Subject: [PATCH 08/44] WIP --- app/(prowler)/providers/(set-up-provider)/layout.tsx | 4 +++- components/providers/radio-group-provider.tsx | 10 +++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/layout.tsx b/app/(prowler)/providers/(set-up-provider)/layout.tsx index b57ca47ef9..788ce9cd96 100644 --- a/app/(prowler)/providers/(set-up-provider)/layout.tsx +++ b/app/(prowler)/providers/(set-up-provider)/layout.tsx @@ -23,7 +23,9 @@ export default function ProviderLayout({ children }: ProviderLayoutProps) {
-
{children}
+
+ {children} +
); diff --git a/components/providers/radio-group-provider.tsx b/components/providers/radio-group-provider.tsx index 6ca3de5263..6b5242dc52 100644 --- a/components/providers/radio-group-provider.tsx +++ b/components/providers/radio-group-provider.tsx @@ -37,7 +37,7 @@ export const CustomRadio: React.FC = (props) => { className={cn( "group inline-flex flex-row-reverse items-center justify-between tap-highlight-transparent hover:opacity-70 active:opacity-50", "max-w-full cursor-pointer gap-4 rounded-lg border-2 border-default p-4", - "data-[selected=true]:border-primar w-full", + "w-full hover:border-action data-[selected=true]:border-action", )} > @@ -78,23 +78,23 @@ export const RadioGroupProvider: React.FC = ({ isInvalid={isInvalid} {...field} > -
+
- AWS + Amazon Web Services
- GCP + Google Cloud Platform
- Azure + Microsoft Azure
From ee7ba35068eef664c9dba6c3aa5cc016a65ed142 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 06:18:21 +0100 Subject: [PATCH 09/44] WIP --- .../workflow/forms/connect-account-form.tsx | 174 +++++++++++++----- components/providers/workflow/workflow.tsx | 11 +- 2 files changed, 133 insertions(+), 52 deletions(-) diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index de5f23c45a..308dc2bc6f 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -1,7 +1,8 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import { SaveIcon } from "lucide-react"; +import { ChevronLeftIcon, ChevronRightIcon, SaveIcon } from "lucide-react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -14,6 +15,9 @@ import { addProviderFormSchema, ApiError } from "../../../../types"; import { RadioGroupProvider } from "../../radio-group-provider"; export const ConnectAccountForm = () => { + const { toast } = useToast(); + const [prevStep, setPrevStep] = useState(1); + const formSchema = addProviderFormSchema; const form = useForm>({ @@ -24,9 +28,7 @@ export const ConnectAccountForm = () => { providerAlias: "", }, }); - - const { toast } = useToast(); - + const providerType = form.watch("providerType"); const isLoading = form.formState.isSubmitting; const onSubmitClient = async (values: z.infer) => { @@ -69,66 +71,144 @@ export const ConnectAccountForm = () => { }); } }); + setPrevStep(1); } }; + const handleNextStep = () => { + setPrevStep((prev) => prev + 1); + }; + + const handleBackStep = () => { + setPrevStep((prev) => prev - 1); + }; + return (
- - - + {prevStep === 1 && ( + <> + {/* Select a provider */} + + {/* Provider UID */} + + {/* Provider alias */} + + + )} + + {prevStep === 2 && ( + <> + {/* Select a provider */} + + {/* Provider UID */} + + {/* Provider alias */} + + + )} +
+ {prevStep === 2 && ( + } + isDisabled={isLoading} + > + Back + + )} -
- Cancel - - - } + startContent={ + !isLoading && + !(prevStep === 1 && providerType === "aws") && ( + + ) + } + endContent={ + !isLoading && + prevStep === 1 && + providerType === "aws" && + } + onPress={() => { + if (prevStep === 1 && providerType === "aws") { + handleNextStep(); + } else { + form.handleSubmit(onSubmitClient)(); + } + }} > - {isLoading ? <>Loading : Next} + {isLoading ? ( + <>Loading + ) : ( + + {prevStep === 1 && providerType === "aws" ? "Next" : "Save"} + + )}
diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx index 20555b3574..28765f8c4b 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow.tsx @@ -10,22 +10,23 @@ const steps = [ { title: "Add your cloud account", description: - "Select the cloud provider of the account you want to connect.", + "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 provider", - description: "Please add your credentials to your cloud provider.", + 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: "Please test your connection to your cloud provider.", + description: + "Test your connection to verify that the credentials provided are valid for accessing your cloud account.", href: "/providers/test-connection", }, { title: "Lunch scan", - description: "Please choose when you want to launch your scan.", + description: "Choose when you want to launch your scan.", href: "/providers", }, ]; From 6783da028cbe76e4881dad135de58e1a893ec27f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 09:49:11 +0100 Subject: [PATCH 10/44] WIP --- components/providers/radio-group-provider.tsx | 49 +-------------- .../workflow/forms/connect-account-form.tsx | 40 +++---------- components/providers/workflow/forms/index.ts | 1 + .../radio-group-aws-via-credentials-form.tsx | 60 +++++++++++++++++++ components/ui/custom/custom-radio.tsx | 48 +++++++++++++++ components/ui/custom/index.ts | 1 + types/formSchemas.ts | 1 + 7 files changed, 122 insertions(+), 78 deletions(-) create mode 100644 components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx create mode 100644 components/ui/custom/custom-radio.tsx diff --git a/components/providers/radio-group-provider.tsx b/components/providers/radio-group-provider.tsx index 6b5242dc52..a90aa4afcd 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,53 +10,9 @@ 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; diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 308dc2bc6f..5b3a5ce7a9 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -13,6 +13,9 @@ import { Form } from "@/components/ui/form"; import { addProvider } from "../../../../actions/providers/providers"; import { addProviderFormSchema, ApiError } from "../../../../types"; import { RadioGroupProvider } from "../../radio-group-provider"; +import { RadioGroupAWSViaCredentialsForm } from "./radio-group-aws-via-credentials-form"; + +export type FormValues = z.infer; export const ConnectAccountForm = () => { const { toast } = useToast(); @@ -20,18 +23,19 @@ export const ConnectAccountForm = () => { const formSchema = addProviderFormSchema; - const form = useForm>({ + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { providerType: "", providerId: "", providerAlias: "", + awsCredentialsType: "", }, }); const providerType = form.watch("providerType"); const isLoading = form.formState.isSubmitting; - const onSubmitClient = async (values: z.infer) => { + const onSubmitClient = async (values: FormValues) => { const formData = new FormData(); Object.entries(values).forEach( @@ -125,37 +129,11 @@ export const ConnectAccountForm = () => { {prevStep === 2 && ( <> - {/* Select a provider */} - - {/* Provider UID */} - - {/* Provider alias */} - + {/* Select AWS credentials type */} + )} +
{prevStep === 2 && ( ; +}; + +export const RadioGroupAWSViaCredentialsForm = ({ + control, +}: RadioGroupAWSViaCredentialsFormProps) => { + return ( + ( + <> + +
+ Using IAM Role + +
+ Connect via CloudFormation +
+
+ +
+ Connect via Terraform +
+
+ + Using Credentials + + +
+ Connect via Credentials +
+
+
+
+ + )} + /> + ); +}; diff --git a/components/ui/custom/custom-radio.tsx b/components/ui/custom/custom-radio.tsx new file mode 100644 index 0000000000..a1248209b1 --- /dev/null +++ b/components/ui/custom/custom-radio.tsx @@ -0,0 +1,48 @@ +import { UseRadioProps } from "@nextui-org/radio/dist/use-radio"; +import { cn, useRadio, VisuallyHidden } from "@nextui-org/react"; +import React from "react"; + +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} + + )} */} +
+
+ ); +}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 703b673471..130646a89e 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -4,4 +4,5 @@ export * from "./custom-button"; export * from "./custom-dropdown-filter"; export * from "./custom-input"; export * from "./custom-loader"; +export * from "./custom-radio"; export * from "./CustomButtonClientAction"; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index e1df1285b8..f541ebf593 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -35,6 +35,7 @@ export const addProviderFormSchema = z.object({ providerType: z.string(), providerAlias: z.string(), providerId: z.string(), + awsCredentialsType: z.string().optional(), }); export const editProviderFormSchema = (currentAlias: string) => From f1a951b2e496db251e611e4d1066d583c0f9fa5a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 15:01:48 +0100 Subject: [PATCH 11/44] chore: add form for add-credentials-providers --- actions/providers/providers.ts | 3 +- .../add-credentials/page.tsx | 12 +- .../connect-account/page.tsx | 6 +- .../workflow/forms/add-credentials-form.tsx | 142 ++++++++++++++++++ .../workflow/forms/connect-account-form.tsx | 6 + components/providers/workflow/forms/index.ts | 1 + components/providers/workflow/workflow.tsx | 2 +- lib/helper.ts | 2 + types/formSchemas.ts | 23 +++ 9 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 components/providers/workflow/forms/add-credentials-form.tsx diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 4578786534..bbf1fbc844 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, @@ -190,6 +190,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/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index 1b9724578b..5ccd9a1f25 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -1,15 +1,7 @@ import React from "react"; -import { NavigationHeader } from "@/components/ui"; +import { AddCredentialsForm } from "@/components/providers/workflow/forms"; export default function AddCredentialsPage() { - return ( - <> - - - ); + return ; } diff --git a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx index 1b4f1bf0f0..67094101f5 100644 --- a/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/connect-account/page.tsx @@ -5,9 +5,5 @@ import React from "react"; import { ConnectAccountForm } from "@/components/providers/workflow/forms"; export default function ConnectAccountPage() { - return ( - <> - - - ); + return ; } diff --git a/components/providers/workflow/forms/add-credentials-form.tsx b/components/providers/workflow/forms/add-credentials-form.tsx new file mode 100644 index 0000000000..567a505310 --- /dev/null +++ b/components/providers/workflow/forms/add-credentials-form.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { SaveIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; + +import { useToast } from "@/components/ui"; +import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; + +import { addProvider } from "../../../../actions/providers/providers"; +import { addCredentialsFormSchema, ApiError } from "../../../../types"; + +export const AddCredentialsForm = ({ + providerType, +}: { + providerType: string; +}) => { + const formSchema = addCredentialsFormSchema(providerType); + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + const isLoading = form.formState.isSubmitting; + + const router = useRouter(); + + 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); + console.log(data); + + 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("providerId", { + 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, + }); + } + }); + } else { + router.push("/providers/test-connection"); + } + }; + + return ( +
+ + {providerType === "aws" && ( + <> + {/* AWS Access Key ID */} + + {/* AWS Secret Access Key */} + + {/* AWS Session Token */} + + + )} + +
+ } + > + {isLoading ? <>Loading : Save} + +
+ + + ); +}; diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 5b3a5ce7a9..5af9b01c12 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ChevronLeftIcon, ChevronRightIcon, SaveIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -35,6 +36,8 @@ export const ConnectAccountForm = () => { const providerType = form.watch("providerType"); const isLoading = form.formState.isSubmitting; + const router = useRouter(); + const onSubmitClient = async (values: FormValues) => { const formData = new FormData(); @@ -43,6 +46,7 @@ export const ConnectAccountForm = () => { ); const data = await addProvider(formData); + console.log(data); if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { @@ -76,6 +80,8 @@ export const ConnectAccountForm = () => { } }); setPrevStep(1); + } else { + router.push("/providers/add-credentials"); } }; diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts index a5c3ae2773..16cd78f184 100644 --- a/components/providers/workflow/forms/index.ts +++ b/components/providers/workflow/forms/index.ts @@ -1,2 +1,3 @@ +export * from "./add-credentials-form"; export * from "./connect-account-form"; export * from "./radio-group-aws-via-credentials-form"; diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx index 28765f8c4b..323d97753d 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow.tsx @@ -25,7 +25,7 @@ const steps = [ href: "/providers/test-connection", }, { - title: "Lunch scan", + title: "Launch scan", description: "Choose when you want to launch your scan.", href: "/providers", }, diff --git a/lib/helper.ts b/lib/helper.ts index 7eeee17a6f..3ce912e078 100644 --- a/lib/helper.ts +++ b/lib/helper.ts @@ -1,5 +1,7 @@ import { MetaDataProps } from "@/types"; +export const wait = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); // Helper function to create dictionaries by type export const createDict = ( type: string, diff --git a/types/formSchemas.ts b/types/formSchemas.ts index f541ebf593..64d6b94718 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -38,6 +38,29 @@ export const addProviderFormSchema = z.object({ awsCredentialsType: z.string().optional(), }); +export const addCredentialsFormSchema = (providerType: string) => + z.object( + providerType === "aws" + ? { + aws_access_key_id: z.string(), + aws_secret_access_key: z.string(), + aws_session_token: z.string(), + } + : providerType === "azure" + ? { + client_id: z.string(), + client_secret: z.string(), + tenant_id: z.string(), + } + : providerType === "gcp" + ? { + client_id: z.string(), + client_secret: z.string(), + refresh_token: z.string(), + } + : {}, + ); + export const editProviderFormSchema = (currentAlias: string) => z.object({ alias: z From 37343750cd719f933db441e8e9b87709fccadc28 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 17:26:34 +0100 Subject: [PATCH 12/44] chore: add form for add-credentials-providers --- actions/providers/providers.ts | 58 +++++++++- .../add-credentials/page.tsx | 8 +- .../workflow/forms/add-credentials-form.tsx | 105 +++++++++++++----- .../workflow/forms/connect-account-form.tsx | 6 +- types/components.ts | 29 +++++ types/formSchemas.ts | 10 +- 6 files changed, 182 insertions(+), 34 deletions(-) diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index bbf1fbc844..842dec1f59 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -148,9 +148,63 @@ 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 aws_access_key_id = formData.get("aws_access_key_id"); + const aws_secret_access_key = formData.get("aws_secret_access_key"); + const aws_session_token = formData.get("aws_session_token"); + const secretName = formData.get("secretName"); + const providerId = formData.get("providerId"); + + const bodyData = { + data: { + type: "ProviderSecret", + attributes: { + secret_type: "static", + secret: { + aws_access_key_id: aws_access_key_id, + aws_secret_access_key: aws_secret_access_key, + aws_session_token: aws_session_token, + }, + 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"); @@ -162,9 +216,11 @@ 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) { diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index 5ccd9a1f25..c47c11e5b5 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -2,6 +2,10 @@ import React from "react"; import { AddCredentialsForm } from "@/components/providers/workflow/forms"; -export default function AddCredentialsPage() { - return ; +export default function AddCredentialsPage({ + searchParams, +}: { + searchParams: { provider: string; id: string }; +}) { + return ; } diff --git a/components/providers/workflow/forms/add-credentials-form.tsx b/components/providers/workflow/forms/add-credentials-form.tsx index 567a505310..e76c443215 100644 --- a/components/providers/workflow/forms/add-credentials-form.tsx +++ b/components/providers/workflow/forms/add-credentials-form.tsx @@ -3,26 +3,57 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { SaveIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; +import { FieldErrors, useForm } from "react-hook-form"; import * as z from "zod"; +import { addCredentialsProvider } from "@/actions/providers/providers"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; -import { addProvider } from "../../../../actions/providers/providers"; -import { addCredentialsFormSchema, ApiError } from "../../../../types"; +import { + addCredentialsFormSchema, + ApiError, + AWSCredentials, + CredentialsFormSchema, +} from "../../../../types"; export const AddCredentialsForm = ({ - providerType, + searchParams, }: { - providerType: string; + searchParams: { provider: string; id: string }; }) => { - const formSchema = addCredentialsFormSchema(providerType); - const { toast } = useToast(); + const providerType = searchParams.provider; + const providerId = searchParams.id; - const form = useForm>({ + const formSchema = addCredentialsFormSchema(providerType); + + const { toast } = useToast(); + const form = useForm({ resolver: zodResolver(formSchema), + defaultValues: { + secretName: "", + providerId, + ...(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: "", + } + : {}), + }, }); const isLoading = form.formState.isSubmitting; @@ -36,28 +67,32 @@ export const AddCredentialsForm = ({ ([key, value]) => value !== undefined && formData.append(key, value), ); - const data = await addProvider(formData); - console.log(data); + 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/provider": - form.setError("providerType", { + case "/data/attributes/secret/aws_access_key_id": + form.setError("aws_access_key_id", { type: "server", message: errorMessage, }); break; - case "/data/attributes/uid": - case "/data/attributes/__all__": - form.setError("providerId", { + case "/data/attributes/secret/aws_secret_access_key": + form.setError("aws_secret_access_key", { type: "server", message: errorMessage, }); break; - case "/data/attributes/alias": - form.setError("providerAlias", { + case "/data/attributes/secret/aws_session_token": + form.setError("aws_session_token", { + type: "server", + message: errorMessage, + }); + break; + case "/data/attributes/name": + form.setError("secretName", { type: "server", message: errorMessage, }); @@ -81,43 +116,61 @@ export const AddCredentialsForm = ({ onSubmit={form.handleSubmit(onSubmitClient)} className="flex flex-col space-y-4" > + + {providerType === "aws" && ( <> - {/* AWS Access Key ID */} ) + .aws_access_key_id + } /> - {/* AWS Secret Access Key */} ) + .aws_secret_access_key + } /> - {/* AWS Session Token */} ) + .aws_session_token + } /> )} diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 5af9b01c12..a2da297ae3 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -81,7 +81,11 @@ export const ConnectAccountForm = () => { }); setPrevStep(1); } else { - router.push("/providers/add-credentials"); + const { + id, + attributes: { provider }, + } = data.data; + router.push(`/providers/add-credentials?provider=${provider}&id=${id}`); } }; diff --git a/types/components.ts b/types/components.ts index 086e6e4a60..6d434f1264 100644 --- a/types/components.ts +++ b/types/components.ts @@ -26,6 +26,35 @@ 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 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 CredentialsFormSchema = + | AWSCredentials + | AzureCredentials + | GCPCredentials; + export interface SearchParamsProps { [key: string]: string | string[] | undefined; } diff --git a/types/formSchemas.ts b/types/formSchemas.ts index 64d6b94718..d9239c1633 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -39,8 +39,10 @@ export const addProviderFormSchema = z.object({ }); export const addCredentialsFormSchema = (providerType: string) => - z.object( - providerType === "aws" + z.object({ + secretName: z.string().optional(), + providerId: z.string(), + ...(providerType === "aws" ? { aws_access_key_id: z.string(), aws_secret_access_key: z.string(), @@ -58,8 +60,8 @@ export const addCredentialsFormSchema = (providerType: string) => client_secret: z.string(), refresh_token: z.string(), } - : {}, - ); + : {}), + }); export const editProviderFormSchema = (currentAlias: string) => z.object({ From d0b599214637e3ddc67fc7b69895c683f9026376 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 30 Oct 2024 17:30:29 +0100 Subject: [PATCH 13/44] feat: redirect on add credentials page if there is no provider associated --- .../(set-up-provider)/add-credentials/page.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index c47c11e5b5..d9d8640703 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -1,11 +1,16 @@ +import { redirect } from "next/navigation"; import React from "react"; import { AddCredentialsForm } from "@/components/providers/workflow/forms"; -export default function AddCredentialsPage({ - searchParams, -}: { +interface Props { searchParams: { provider: string; id: string }; -}) { +} + +export default function AddCredentialsPage({ searchParams }: Props) { + if (!searchParams.provider || !searchParams.id) { + redirect("/providers/connect-account"); + } + return ; } From 3a8053c3c6fd057a61a5030d88229b8c30dcd0f9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 05:53:59 +0100 Subject: [PATCH 14/44] chore: remove the old form to add providers --- components/providers/add-provider.tsx | 41 ++------ components/providers/forms/add-form.tsx | 127 ------------------------ components/providers/forms/index.ts | 1 - 3 files changed, 11 insertions(+), 158 deletions(-) delete mode 100644 components/providers/forms/add-form.tsx diff --git a/components/providers/add-provider.tsx b/components/providers/add-provider.tsx index 3033f8b904..01e85db980 100644 --- a/components/providers/add-provider.tsx +++ b/components/providers/add-provider.tsx @@ -1,39 +1,20 @@ "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 ( -
- - - - - -
- setIsOpen(false)} - isDisabled={isLoading} - > - Cancel - - - } - > - {isLoading ? <>Loading : Confirm} - -
- - - ); -}; 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"; From 052b8821956eae655ef269b46c39c4dd9060f936 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 06:41:36 +0100 Subject: [PATCH 15/44] chore: client validation when select a provider type --- components/providers/radio-group-provider.tsx | 9 +++- .../workflow/forms/connect-account-form.tsx | 14 ++++--- .../radio-group-aws-via-credentials-form.tsx | 17 +++++++- types/formSchemas.ts | 42 ++++++++++++++++--- 4 files changed, 69 insertions(+), 13 deletions(-) diff --git a/components/providers/radio-group-provider.tsx b/components/providers/radio-group-provider.tsx index a90aa4afcd..e376e69ff4 100644 --- a/components/providers/radio-group-provider.tsx +++ b/components/providers/radio-group-provider.tsx @@ -16,11 +16,13 @@ import { FormMessage } from "../ui/form"; interface RadioGroupProviderProps { control: Control>; isInvalid: boolean; + errorMessage?: string; } export const RadioGroupProvider: React.FC = ({ control, isInvalid, + errorMessage, }) => { return ( = ({ className="flex flex-wrap" isInvalid={isInvalid} {...field} + value={field.value || ""} >
@@ -60,7 +63,11 @@ export const RadioGroupProvider: React.FC = ({
- + {errorMessage && ( + + {errorMessage} + + )} )} /> diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index a2da297ae3..428fe46992 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -21,13 +21,14 @@ export type FormValues = z.infer; 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: "", + providerType: undefined, providerId: "", providerAlias: "", awsCredentialsType: "", @@ -36,9 +37,8 @@ export const ConnectAccountForm = () => { const providerType = form.watch("providerType"); const isLoading = form.formState.isSubmitting; - const router = useRouter(); - const onSubmitClient = async (values: FormValues) => { + console.log({ values }); const formData = new FormData(); Object.entries(values).forEach( @@ -46,7 +46,6 @@ export const ConnectAccountForm = () => { ); const data = await addProvider(formData); - console.log(data); if (data?.errors && data.errors.length > 0) { data.errors.forEach((error: ApiError) => { @@ -109,6 +108,7 @@ export const ConnectAccountForm = () => { {/* Provider UID */} { {prevStep === 2 && ( <> {/* Select AWS credentials type */} - + )} 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 index fae21b2482..a0a04f551d 100644 --- a/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx +++ b/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx @@ -5,15 +5,20 @@ 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
+ {errorMessage && ( + + {errorMessage} + + )} )} /> diff --git a/types/formSchemas.ts b/types/formSchemas.ts index d9239c1633..5297692124 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -31,12 +31,42 @@ export const scheduleScanFormSchema = () => scheduleDate: z.string(), }); -export const addProviderFormSchema = z.object({ - providerType: z.string(), - providerAlias: z.string(), - providerId: z.string(), - awsCredentialsType: z.string().optional(), -}); +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(), + providerId: 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(), + providerId: z.string(), + awsCredentialsType: z.string().optional(), + }), + z.object({ + providerType: z.literal("gcp"), + providerAlias: z.string(), + providerId: z.string(), + awsCredentialsType: z.string().optional(), + }), + z.object({ + providerType: z.literal("kubernetes"), + providerAlias: z.string(), + providerId: z.string(), + awsCredentialsType: z.string().optional(), + }), + ]), + ); export const addCredentialsFormSchema = (providerType: string) => z.object({ From 9882cd53cf5bed440c192bd95a7409af8109a14a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 06:48:58 +0100 Subject: [PATCH 16/44] chore: add credentials type to the url if exists --- .../providers/workflow/forms/connect-account-form.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 428fe46992..063c4403b4 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -84,7 +84,12 @@ export const ConnectAccountForm = () => { id, attributes: { provider }, } = data.data; - router.push(`/providers/add-credentials?provider=${provider}&id=${id}`); + const credentialsParam = values.awsCredentialsType + ? `&via=${values.awsCredentialsType}` + : ""; + router.push( + `/providers/add-credentials?provider=${provider}&id=${id}${credentialsParam}`, + ); } }; From 7c4f34bb6cbc3b1dd361a72c7ca90a1a75e35f12 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 08:08:20 +0100 Subject: [PATCH 17/44] feat: custom add credentials page --- .../add-credentials/page.tsx | 20 +++++++-- components/providers/workflow/forms/index.ts | 2 +- ...ials-form.tsx => via-credentials-form.tsx} | 41 ++++++++++++------- components/ui/custom/custom-input.tsx | 3 ++ 4 files changed, 46 insertions(+), 20 deletions(-) rename components/providers/workflow/forms/{add-credentials-form.tsx => via-credentials-form.tsx} (90%) diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index d9d8640703..83ae37a99a 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -1,16 +1,28 @@ import { redirect } from "next/navigation"; import React from "react"; -import { AddCredentialsForm } from "@/components/providers/workflow/forms"; +import { ViaCredentialsForm } from "@/components/providers/workflow/forms"; interface Props { - searchParams: { provider: string; id: string }; + searchParams: { provider: string; id: string; via?: string }; } export default function AddCredentialsPage({ searchParams }: Props) { - if (!searchParams.provider || !searchParams.id) { + if ( + !searchParams.provider || + !searchParams.id || + (searchParams.provider === "aws" && !searchParams.via) + ) { redirect("/providers/connect-account"); } - return ; + const useCredentialsForm = + (searchParams.provider === "aws" && searchParams.via === "credentials") || + (searchParams.provider !== "aws" && !searchParams.via); + + return ( + <> + {useCredentialsForm && } + + ); } diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts index 16cd78f184..b4bb037a59 100644 --- a/components/providers/workflow/forms/index.ts +++ b/components/providers/workflow/forms/index.ts @@ -1,3 +1,3 @@ -export * from "./add-credentials-form"; export * from "./connect-account-form"; export * from "./radio-group-aws-via-credentials-form"; +export * from "./via-credentials-form"; diff --git a/components/providers/workflow/forms/add-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx similarity index 90% rename from components/providers/workflow/forms/add-credentials-form.tsx rename to components/providers/workflow/forms/via-credentials-form.tsx index e76c443215..52913beab9 100644 --- a/components/providers/workflow/forms/add-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -18,17 +18,19 @@ import { CredentialsFormSchema, } from "../../../../types"; -export const AddCredentialsForm = ({ +export const ViaCredentialsForm = ({ searchParams, }: { searchParams: { provider: string; id: string }; }) => { + const router = useRouter(); + const { toast } = useToast(); + const providerType = searchParams.provider; const providerId = searchParams.id; const formSchema = addCredentialsFormSchema(providerType); - const { toast } = useToast(); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -58,8 +60,6 @@ export const AddCredentialsForm = ({ const isLoading = form.formState.isSubmitting; - const router = useRouter(); - const onSubmitClient = async (values: z.infer) => { const formData = new FormData(); @@ -117,19 +117,17 @@ export const AddCredentialsForm = ({ className="flex flex-col space-y-4" > - + {providerType === "aws" && ( <> +
+
+ Connect via Credentials +
+
+ Please provide the information for your AWS credentials. +
+
)} + Name (Optional) +
{ label?: string; labelPlacement?: "inside" | "outside"; variant?: "flat" | "bordered" | "underlined" | "faded"; + size?: "sm" | "md" | "lg"; type?: string; placeholder?: string; password?: boolean; @@ -29,6 +30,7 @@ export const CustomInput = ({ labelPlacement = "inside", placeholder, variant = "bordered", + size = "md", confirmPassword = false, password = false, isRequired = true, @@ -95,6 +97,7 @@ export const CustomInput = ({ placeholder={inputPlaceholder} type={inputType} variant={variant} + size={size} isInvalid={isInvalid} endContent={endContent} {...field} From fb99733a1e78dd2cc255fd88e046cd2846f6af62 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 08:51:44 +0100 Subject: [PATCH 18/44] chore: add form for azure credentials --- .../workflow/forms/via-credentials-form.tsx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 52913beab9..5c500d3818 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -15,6 +15,7 @@ import { addCredentialsFormSchema, ApiError, AWSCredentials, + AzureCredentials, CredentialsFormSchema, } from "../../../../types"; @@ -172,6 +173,60 @@ export const ViaCredentialsForm = ({ /> )} + {providerType === "azure" && ( + <> +
+
+ Connect via Credentials +
+
+ Please provide the information for your Azure credentials. +
+
+ ) + .client_id + } + /> + ) + .client_secret + } + /> + ) + .tenant_id + } + /> + + )} Name (Optional) Date: Thu, 31 Oct 2024 09:08:23 +0100 Subject: [PATCH 19/44] chore: create separate component for aws credentials --- .../workflow/forms/via-credentials-form.tsx | 58 ++-------------- .../via-credentials/aws-credentials-form.tsx | 66 +++++++++++++++++++ .../workflow/forms/via-credentials/index.ts | 1 + 3 files changed, 72 insertions(+), 53 deletions(-) create mode 100644 components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx create mode 100644 components/providers/workflow/forms/via-credentials/index.ts diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 5c500d3818..58394aeb48 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { SaveIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import { FieldErrors, useForm } from "react-hook-form"; +import { Control, FieldErrors, useForm } from "react-hook-form"; import * as z from "zod"; import { addCredentialsProvider } from "@/actions/providers/providers"; @@ -18,6 +18,7 @@ import { AzureCredentials, CredentialsFormSchema, } from "../../../../types"; +import { AWScredentialsForm } from "./via-credentials/aws-credentials-form"; export const ViaCredentialsForm = ({ searchParams, @@ -120,58 +121,9 @@ export const ViaCredentialsForm = ({ {providerType === "aws" && ( - <> -
-
- Connect via Credentials -
-
- Please provide the information for your AWS credentials. -
-
- ) - .aws_access_key_id - } - /> - ) - .aws_secret_access_key - } - /> - ) - .aws_session_token - } - /> - + } + /> )} {providerType === "azure" && ( <> 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..ab46cb33b7 --- /dev/null +++ b/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx @@ -0,0 +1,66 @@ +import { Control, FieldErrors } from "react-hook-form"; + +import { CustomInput } from "@/components/ui/custom"; + +import { AWSCredentials } from "../../../../../types"; + +interface AWScredentialsFormProps { + control: Control; +} + +export const AWScredentialsForm = ({ control }: AWScredentialsFormProps) => { + return ( + <> +
+
+ Connect via Credentials +
+
+ Please provide the information for your AWS credentials. +
+
+ ) + .aws_access_key_id + } + /> + ) + .aws_secret_access_key + } + /> + ) + .aws_session_token + } + /> + + ); +}; 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..c247168990 --- /dev/null +++ b/components/providers/workflow/forms/via-credentials/index.ts @@ -0,0 +1 @@ +export * from "./aws-credentials-form"; From c81cb04bd06b3dc5d5fbd29a40b058b38eb1a73a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 09:12:31 +0100 Subject: [PATCH 20/44] chore: create separate component for azure credentials --- .../workflow/forms/via-credentials-form.tsx | 58 ++-------------- .../azure-credentials-form.tsx | 68 +++++++++++++++++++ .../workflow/forms/via-credentials/index.ts | 1 + 3 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 58394aeb48..cbdf12c760 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -3,7 +3,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { SaveIcon } from "lucide-react"; import { useRouter } from "next/navigation"; -import { Control, FieldErrors, useForm } from "react-hook-form"; +import { Control, useForm } from "react-hook-form"; import * as z from "zod"; import { addCredentialsProvider } from "@/actions/providers/providers"; @@ -19,6 +19,7 @@ import { CredentialsFormSchema, } from "../../../../types"; import { AWScredentialsForm } from "./via-credentials/aws-credentials-form"; +import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; export const ViaCredentialsForm = ({ searchParams, @@ -126,58 +127,9 @@ export const ViaCredentialsForm = ({ /> )} {providerType === "azure" && ( - <> -
-
- Connect via Credentials -
-
- Please provide the information for your Azure credentials. -
-
- ) - .client_id - } - /> - ) - .client_secret - } - /> - ) - .tenant_id - } - /> - + } + /> )} Name (Optional) ; +} + +export const AzureCredentialsForm = ({ + control, +}: AzureCredentialsFormProps) => { + return ( + <> +
+
+ Connect via Credentials +
+
+ Please provide the information for your Azure credentials. +
+
+ ) + .client_id + } + /> + ) + .client_secret + } + /> + ) + .tenant_id + } + /> + + ); +}; diff --git a/components/providers/workflow/forms/via-credentials/index.ts b/components/providers/workflow/forms/via-credentials/index.ts index c247168990..4286cf7741 100644 --- a/components/providers/workflow/forms/via-credentials/index.ts +++ b/components/providers/workflow/forms/via-credentials/index.ts @@ -1 +1,2 @@ export * from "./aws-credentials-form"; +export * from "./azure-credentials-form"; From 593bce5155218584758ad87f1882fe3c15a953d2 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 18:27:28 +0100 Subject: [PATCH 21/44] feat: add credentials for AWS and Azure are working nicely --- actions/providers/providers.ts | 27 +++++++--- app/(prowler)/page.tsx | 10 ++-- .../workflow/forms/via-credentials-form.tsx | 49 ++++++++++++++----- .../via-credentials/aws-credentials-form.tsx | 30 ++++-------- .../azure-credentials-form.tsx | 28 +++-------- types/formSchemas.ts | 23 +++++---- 6 files changed, 90 insertions(+), 77 deletions(-) diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 842dec1f59..f0fe26b0d5 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -148,27 +148,38 @@ 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 aws_access_key_id = formData.get("aws_access_key_id"); - const aws_secret_access_key = formData.get("aws_secret_access_key"); - const aws_session_token = formData.get("aws_session_token"); const secretName = formData.get("secretName"); const providerId = formData.get("providerId"); + const providerType = formData.get("providerType"); + + let secret = {}; + + if (providerType === "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") { + secret = { + client_id: formData.get("client_id"), + client_secret: formData.get("client_secret"), + tenant_id: formData.get("tenant_id"), + }; + } const bodyData = { data: { type: "ProviderSecret", attributes: { secret_type: "static", - secret: { - aws_access_key_id: aws_access_key_id, - aws_secret_access_key: aws_secret_access_key, - aws_session_token: aws_session_token, - }, + secret, name: secretName, }, relationships: { 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/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index cbdf12c760..c406e0574a 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -10,17 +10,19 @@ import { addCredentialsProvider } from "@/actions/providers/providers"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; +import { addCredentialsFormSchema, ApiError, AzureCredentials } from "@/types"; +import { AWSCredentials } from "@/types"; -import { - addCredentialsFormSchema, - ApiError, - AWSCredentials, - AzureCredentials, - CredentialsFormSchema, -} from "../../../../types"; import { AWScredentialsForm } from "./via-credentials/aws-credentials-form"; import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; +type CredentialsFormSchema = z.infer< + ReturnType +>; + +// Add this type intersection to include all fields +type FormType = CredentialsFormSchema & AWSCredentials & AzureCredentials; + export const ViaCredentialsForm = ({ searchParams, }: { @@ -31,14 +33,14 @@ export const ViaCredentialsForm = ({ const providerType = searchParams.provider; const providerId = searchParams.id; - const formSchema = addCredentialsFormSchema(providerType); - const form = useForm({ + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { secretName: "", providerId, + providerType, ...(providerType === "aws" ? { aws_access_key_id: "", @@ -63,7 +65,8 @@ export const ViaCredentialsForm = ({ const isLoading = form.formState.isSubmitting; - const onSubmitClient = async (values: z.infer) => { + const onSubmitClient = async (values: FormType) => { + console.log("via credentials form", values); const formData = new FormData(); Object.entries(values).forEach( @@ -94,6 +97,24 @@ export const ViaCredentialsForm = ({ 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/name": form.setError("secretName", { type: "server", @@ -109,7 +130,7 @@ export const ViaCredentialsForm = ({ } }); } else { - router.push("/providers/test-connection"); + router.push(`/providers/test-connection?id=${providerId}`); } }; @@ -120,17 +141,19 @@ export const ViaCredentialsForm = ({ className="flex flex-col space-y-4" > + {providerType === "aws" && ( } + control={form.control as unknown as Control} /> )} {providerType === "azure" && ( } + control={form.control as unknown as Control} /> )} + Name (Optional) ; -} - -export const AWScredentialsForm = ({ control }: AWScredentialsFormProps) => { +}) => { return ( <>
@@ -28,10 +27,7 @@ export const AWScredentialsForm = ({ control }: AWScredentialsFormProps) => { placeholder="Enter the AWS Access Key ID" variant="bordered" isRequired - isInvalid={ - !!(control._formState.errors as FieldErrors) - .aws_access_key_id - } + isInvalid={!!control._formState.errors.aws_access_key_id} /> { placeholder="Enter the AWS Secret Access Key" variant="bordered" isRequired - isInvalid={ - !!(control._formState.errors as FieldErrors) - .aws_secret_access_key - } + isInvalid={!!control._formState.errors.aws_secret_access_key} /> { labelPlacement="inside" placeholder="Enter the AWS Session Token" variant="bordered" - isRequired - isInvalid={ - !!(control._formState.errors as FieldErrors) - .aws_session_token - } + isRequired={false} + isInvalid={!!control._formState.errors.aws_session_token} /> ); diff --git a/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx index b9d3046635..87b9fb6b66 100644 --- a/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials/azure-credentials-form.tsx @@ -1,16 +1,13 @@ -import { Control, FieldErrors } from "react-hook-form"; +import { Control } from "react-hook-form"; import { CustomInput } from "@/components/ui/custom"; - -import { AzureCredentials } from "../../../../../types"; - -interface AzureCredentialsFormProps { - control: Control; -} +import { AzureCredentials } from "@/types"; export const AzureCredentialsForm = ({ control, -}: AzureCredentialsFormProps) => { +}: { + control: Control; +}) => { return ( <>
@@ -30,10 +27,7 @@ export const AzureCredentialsForm = ({ placeholder="Enter the Client ID" variant="bordered" isRequired - isInvalid={ - !!(control._formState.errors as FieldErrors) - .client_id - } + isInvalid={!!control._formState.errors.client_id} /> ) - .client_secret - } + isInvalid={!!control._formState.errors.client_secret} /> ) - .tenant_id - } + isInvalid={!!control._formState.errors.tenant_id} /> ); diff --git a/types/formSchemas.ts b/types/formSchemas.ts index 5297692124..bffffbaf0f 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -72,23 +72,28 @@ 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(), - aws_secret_access_key: z.string(), - aws_session_token: z.string(), + 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(), - client_secret: z.string(), - tenant_id: z.string(), + 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(), - client_secret: z.string(), - refresh_token: z.string(), + 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"), } : {}), }); From 33ae08be65a533b14d6e315ebf459668f4f40cf8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 31 Oct 2024 18:38:12 +0100 Subject: [PATCH 22/44] feat: add credentials for GCP --- actions/providers/providers.ts | 6 ++ .../workflow/forms/via-credentials-form.tsx | 15 ++++- .../via-credentials/gcp-credentials-form.tsx | 56 +++++++++++++++++++ .../workflow/forms/via-credentials/index.ts | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 components/providers/workflow/forms/via-credentials/gcp-credentials-form.tsx diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index f0fe26b0d5..58975d7987 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -172,6 +172,12 @@ export const addCredentialsProvider = async (formData: FormData) => { client_secret: formData.get("client_secret"), tenant_id: formData.get("tenant_id"), }; + } else if (providerType === "gcp") { + secret = { + client_id: formData.get("client_id"), + client_secret: formData.get("client_secret"), + refresh_token: formData.get("refresh_token"), + }; } const bodyData = { diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index c406e0574a..105962c53c 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -10,11 +10,17 @@ import { addCredentialsProvider } from "@/actions/providers/providers"; import { useToast } from "@/components/ui"; import { CustomButton, CustomInput } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; -import { addCredentialsFormSchema, ApiError, AzureCredentials } from "@/types"; -import { AWSCredentials } from "@/types"; +import { + addCredentialsFormSchema, + ApiError, + AWSCredentials, + AzureCredentials, + GCPCredentials, +} 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"; type CredentialsFormSchema = z.infer< ReturnType @@ -153,6 +159,11 @@ export const ViaCredentialsForm = ({ control={form.control as unknown as Control} /> )} + {providerType === "gcp" && ( + } + /> + )} Name (Optional) ; +}) => { + 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 index 4286cf7741..8ae3137774 100644 --- a/components/providers/workflow/forms/via-credentials/index.ts +++ b/components/providers/workflow/forms/via-credentials/index.ts @@ -1,2 +1,3 @@ export * from "./aws-credentials-form"; export * from "./azure-credentials-form"; +export * from "./gcp-credentials-form"; From c3c775786cdedc6f27eb136ddbe176ea6139f67a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 1 Nov 2024 09:30:43 +0100 Subject: [PATCH 23/44] feat: add credentials for kubernetes --- actions/providers/providers.ts | 4 +++ .../workflow/forms/via-credentials-form.tsx | 25 ++++++++++++-- .../workflow/forms/via-credentials/index.ts | 1 + .../via-credentials/k8s-credentials-form.tsx | 34 +++++++++++++++++++ types/components.ts | 9 ++++- types/formSchemas.ts | 8 ++++- 6 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 components/providers/workflow/forms/via-credentials/k8s-credentials-form.tsx diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 58975d7987..c4b654f1bb 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -178,6 +178,10 @@ export const addCredentialsProvider = async (formData: FormData) => { client_secret: formData.get("client_secret"), refresh_token: formData.get("refresh_token"), }; + } else if (providerType === "kubernetes") { + secret = { + kubeconfig_content: formData.get("kubeconfig_content"), + }; } const bodyData = { diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 105962c53c..5a6524ce0a 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -16,18 +16,24 @@ import { 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; +type FormType = CredentialsFormSchema & + AWSCredentials & + AzureCredentials & + GCPCredentials & + KubernetesCredentials; export const ViaCredentialsForm = ({ searchParams, @@ -65,7 +71,11 @@ export const ViaCredentialsForm = ({ client_secret: "", refresh_token: "", } - : {}), + : providerType === "kubernetes" + ? { + kubeconfig_content: "", + } + : {}), }, }); @@ -121,6 +131,12 @@ export const ViaCredentialsForm = ({ 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", @@ -164,6 +180,11 @@ export const ViaCredentialsForm = ({ control={form.control as unknown as Control} /> )} + {providerType === "kubernetes" && ( + } + /> + )} Name (Optional) ; +}) => { + return ( + <> +
+
+ Connect via Credentials +
+
+ Please provide the information for your Kubernetes credentials. +
+
+ + + ); +}; diff --git a/types/components.ts b/types/components.ts index 6d434f1264..9c84ce65ab 100644 --- a/types/components.ts +++ b/types/components.ts @@ -50,10 +50,17 @@ export type GCPCredentials = { providerId: string; }; +export type KubernetesCredentials = { + kubeconfig_content: string; + secretName: string; + providerId: string; +}; + export type CredentialsFormSchema = | AWSCredentials | AzureCredentials - | GCPCredentials; + | GCPCredentials + | KubernetesCredentials; export interface SearchParamsProps { [key: string]: string | string[] | undefined; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index bffffbaf0f..ee15b5a16b 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -95,7 +95,13 @@ export const addCredentialsFormSchema = (providerType: string) => 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 editProviderFormSchema = (currentAlias: string) => From 89c441ba584555cde6e7bc617964520e73fab9c8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 2 Nov 2024 09:10:30 +0100 Subject: [PATCH 24/44] feat: add test connection form --- actions/providers/providers.ts | 2 +- .../test-connection/page.tsx | 15 +++- components/providers/workflow/forms/index.ts | 1 + .../workflow/forms/test-connection-form.tsx | 78 +++++++++++++++++++ types/formSchemas.ts | 4 + 5 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 components/providers/workflow/forms/test-connection-form.tsx diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index c4b654f1bb..55fb052d1f 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -228,7 +228,7 @@ export const checkConnectionProvider = async (formData: FormData) => { 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`); diff --git a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx index 8138766f63..943219bee2 100644 --- a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -1,5 +1,16 @@ +import { redirect } from "next/navigation"; import React from "react"; -export default function TestConnectionPage() { - return
TestConnectionPage
; +import { TestConnectionForm } from "@/components/providers/workflow/forms"; + +interface Props { + searchParams: { id: string }; +} + +export default function TestConnectionPage({ searchParams }: Props) { + if (!searchParams.id) { + redirect("/providers/connect-account"); + } + + return ; } diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts index b4bb037a59..539efb24ed 100644 --- a/components/providers/workflow/forms/index.ts +++ b/components/providers/workflow/forms/index.ts @@ -1,3 +1,4 @@ export * from "./connect-account-form"; export * from "./radio-group-aws-via-credentials-form"; +export * from "./test-connection-form"; export * from "./via-credentials-form"; 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..2b2d085564 --- /dev/null +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +// import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { checkConnectionProvider } from "@/actions/providers/providers"; +import { SaveIcon } from "@/components/icons"; +// import { useToast } from "@/components/ui"; +import { CustomButton } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { testConnectionFormSchema } from "@/types"; + +type FormValues = z.infer; +export const TestConnectionForm = ({ + searchParams, +}: { + searchParams: { id: string }; +}) => { + // const { toast } = useToast(); + // const router = useRouter(); + const providerId = searchParams.id; + + const formSchema = testConnectionFormSchema; + + 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) { + console.log({ data }, "error"); + } else { + console.log({ data }, "success"); + } + }; + return ( +
+ +
+
+ Test connection +
+
+ Please test the connection to the provider. +
+
+ +
+ } + > + {isLoading ? <>Loading : Connect account} + +
+
+ + ); +}; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index ee15b5a16b..da3dd3d8d9 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -104,6 +104,10 @@ export const addCredentialsFormSchema = (providerType: string) => : {}), }); +export const testConnectionFormSchema = z.object({ + providerId: z.string(), +}); + export const editProviderFormSchema = (currentAlias: string) => z.object({ alias: z From ee2d7ca79e13e686b6b170e1d10c5dc8195cec4f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 2 Nov 2024 10:08:00 +0100 Subject: [PATCH 25/44] feat: add test connection form --- .../workflow/forms/test-connection-form.tsx | 84 ++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index 2b2d085564..d8f4a1c46e 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -1,28 +1,32 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { Icon } from "@iconify/react"; +import Link from "next/link"; +import { useState } from "react"; // import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { checkConnectionProvider } from "@/actions/providers/providers"; import { SaveIcon } from "@/components/icons"; -// import { useToast } from "@/components/ui"; +import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; -import { testConnectionFormSchema } from "@/types"; +import { ApiError, testConnectionFormSchema } from "@/types"; type FormValues = z.infer; + export const TestConnectionForm = ({ searchParams, }: { searchParams: { id: string }; }) => { - // const { toast } = useToast(); - // const router = useRouter(); + const { toast } = useToast(); const providerId = searchParams.id; const formSchema = testConnectionFormSchema; + const [apiErrorMessage, setApiErrorMessage] = useState(null); const form = useForm({ resolver: zodResolver(formSchema), @@ -41,36 +45,76 @@ export const TestConnectionForm = ({ const data = await checkConnectionProvider(formData); if (data?.errors && data.errors.length > 0) { - console.log({ data }, "error"); + 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 { - console.log({ data }, "success"); + console.log({ data: data.data.id }, "success"); + setApiErrorMessage(null); } }; + return (
- +
Test connection
- Please test the connection to the provider. + Please check the provider connection
+ {apiErrorMessage && ( +
+

{`Provider ID ${apiErrorMessage.toLowerCase()}. Please check and try again.`}

+
+ )} + + +
- } - > - {isLoading ? <>Loading : Connect account} - + {apiErrorMessage ? ( + + + Back to providers + + ) : ( + } + > + {isLoading ? <>Loading : Connect account} + + )}
From 4a3b767002b341cba624c6ce6d0b104a699005e7 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sun, 3 Nov 2024 10:07:11 +0100 Subject: [PATCH 26/44] chore: remove the old test connection component --- .../providers/CheckConnectionProvider.tsx | 42 ------------------- components/providers/index.ts | 1 - .../table/data-table-row-actions.tsx | 3 +- 3 files changed, 1 insertion(+), 45 deletions(-) delete mode 100644 components/providers/CheckConnectionProvider.tsx 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/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/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 35f14d395c..127e829c4a 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"; @@ -80,7 +79,7 @@ export function DataTableRowActions({ textValue="Check Connection" startContent={} > - + {/* TODO: add the provider type to the search params */} Date: Sun, 3 Nov 2024 10:39:10 +0100 Subject: [PATCH 27/44] chore: update with the last step - workflow component --- .../test-connection/page.tsx | 2 +- .../workflow/forms/test-connection-form.tsx | 45 ++++++++++++++++++- components/providers/workflow/workflow.tsx | 5 ++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx index 943219bee2..e3bdb29928 100644 --- a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -4,7 +4,7 @@ import React from "react"; import { TestConnectionForm } from "@/components/providers/workflow/forms"; interface Props { - searchParams: { id: string }; + searchParams: { type: string; id: string }; } export default function TestConnectionPage({ searchParams }: Props) { diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index d8f4a1c46e..5a0bc394c6 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -3,12 +3,13 @@ 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 { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { checkConnectionProvider } from "@/actions/providers/providers"; +import { getTask } from "@/actions/task/tasks"; import { SaveIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; @@ -20,13 +21,19 @@ type FormValues = z.infer; export const TestConnectionForm = ({ searchParams, }: { - searchParams: { id: string }; + searchParams: { type: string; id: string }; }) => { const { toast } = useToast(); + const router = useRouter(); + const providerType = searchParams.type; const providerId = searchParams.id; const formSchema = testConnectionFormSchema; const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [connectionStatus, setConnectionStatus] = useState<{ + connected: boolean; + error: string | null; + } | null>(null); const form = useForm({ resolver: zodResolver(formSchema), @@ -62,7 +69,25 @@ export const TestConnectionForm = ({ }); } else { console.log({ data: data.data.id }, "success"); + const taskId = data.data.id; setApiErrorMessage(null); + + const task = await getTask(taskId); + console.log({ task }, "task"); + + const connected = task.data.attributes.result.connected; + const error = task.data.attributes.result.error; + + setConnectionStatus({ + connected, + error, + }); + + if (connected) { + router.push( + `/providers/launch-scan?type=${providerType}&id=${providerId}&connected=${connected}`, + ); + } } }; @@ -87,6 +112,22 @@ export const TestConnectionForm = ({
)} + {connectionStatus && !connectionStatus.connected && ( +
+
+ +
+
+

+ {connectionStatus.error || "Unknown error"} +

+
+
+ )} +
diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx index 323d97753d..516ba4e58b 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow.tsx @@ -26,8 +26,9 @@ const steps = [ }, { title: "Launch scan", - description: "Choose when you want to launch your scan.", - href: "/providers", + description: + "Launch the scan now or schedule it for a later date and time.", + href: "/providers/launch-scan", }, ]; From 258d18112cf72e0dd6b146cab6c6bd6b4e24d873 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sun, 3 Nov 2024 11:31:47 +0100 Subject: [PATCH 28/44] feat: add action to getTask and implement the last step in the workflow - launch scan --- actions/task/index.ts | 1 + actions/task/tasks.ts | 24 ++++ .../(set-up-provider)/launch-scan/page.tsx | 33 +++++ components/providers/workflow/forms/index.ts | 1 + .../workflow/forms/launch-scan-form.tsx | 113 ++++++++++++++++++ .../workflow/forms/via-credentials-form.tsx | 4 +- types/formSchemas.ts | 11 ++ 7 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 actions/task/index.ts create mode 100644 actions/task/tasks.ts create mode 100644 app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx create mode 100644 components/providers/workflow/forms/launch-scan-form.tsx 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)/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..0fd9edb08c --- /dev/null +++ b/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx @@ -0,0 +1,33 @@ +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; connected: boolean }; +} + +export default async function LaunchScanPage({ searchParams }: Props) { + const providerId = searchParams.id; + const connectedSearchParam = searchParams.connected; + + 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 || connectedSearchParam !== isConnected.toString()) { + redirect("/providers/connect-account"); + } + + return ( + + ); +} diff --git a/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts index 539efb24ed..cf0417ed09 100644 --- a/components/providers/workflow/forms/index.ts +++ b/components/providers/workflow/forms/index.ts @@ -1,4 +1,5 @@ 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"; 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..478cef5f62 --- /dev/null +++ b/components/providers/workflow/forms/launch-scan-form.tsx @@ -0,0 +1,113 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { SaveIcon } from "@/components/icons"; +// import { useToast } from "@/components/ui"; +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; connected: boolean }; + providerData: { + data: { + type: string; + id: string; + attributes: ProviderProps["attributes"]; + }; + }; +} + +export const LaunchScanForm = ({ + searchParams, + providerData, +}: LaunchScanFormProps) => { + const providerType = searchParams.type; + const providerId = searchParams.id; + const connected = searchParams.connected; + + // const [apiErrorMessage, setApiErrorMessage] = useState(null); + + const formSchema = launchScanFormSchema(); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + providerId, + providerType, + scannerArgs: { + checksToExecute: [], + }, + }, + }); + + console.log({ providerData }); + + const isLoading = form.formState.isSubmitting; + + const onSubmitClient = async (values: FormValues) => { + console.log({ values }, "values from test connection form"); + + if (connected) { + console.log("connected"); + } else { + console.log("not connected"); + } + }; + + return ( +
+ +
+
+ Launch scan +
+
+ Launch the scan now or schedule it for a later date and time. +
+
+ + {/* {apiErrorMessage && ( +
+

{`Provider ID ${apiErrorMessage.toLowerCase()}. Please check and try again.`}

+
+ )} */} + + + + + + +
+ } + > + {isLoading ? <>Loading : Launch scan} + +
+ + + ); +}; diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 5a6524ce0a..fb2507e304 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -152,7 +152,9 @@ export const ViaCredentialsForm = ({ } }); } else { - router.push(`/providers/test-connection?id=${providerId}`); + router.push( + `/providers/test-connection?type=${providerType}&id=${providerId}`, + ); } }; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index da3dd3d8d9..30877d5894 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -108,6 +108,17 @@ 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 From 8ca21bb92e9cd6a3c6ed216c8046b5de1cf36374 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 4 Nov 2024 07:46:44 +0100 Subject: [PATCH 29/44] chore: add alias by default if provider alias is empty when add a provider --- actions/providers/providers.ts | 4 ++-- .../workflow/forms/connect-account-form.tsx | 23 ++++++++++++++----- types/formSchemas.ts | 8 +++---- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 55fb052d1f..a1704c4fea 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -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, }, }, diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 063c4403b4..53e4cc24ef 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -29,7 +29,7 @@ export const ConnectAccountForm = () => { resolver: zodResolver(formSchema), defaultValues: { providerType: undefined, - providerId: "", + providerUid: "", providerAlias: "", awsCredentialsType: "", }, @@ -38,10 +38,21 @@ export const ConnectAccountForm = () => { const isLoading = form.formState.isSubmitting; const onSubmitClient = async (values: FormValues) => { - console.log({ values }); + 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}`; + } + + console.log({ formValues }); const formData = new FormData(); - Object.entries(values).forEach( + Object.entries(formValues).forEach( ([key, value]) => value !== undefined && formData.append(key, value), ); @@ -59,7 +70,7 @@ export const ConnectAccountForm = () => { break; case "/data/attributes/uid": case "/data/attributes/__all__": - form.setError("providerId", { + form.setError("providerUid", { type: "server", message: errorMessage, }); @@ -118,14 +129,14 @@ export const ConnectAccountForm = () => { {/* Provider UID */} {/* Provider alias */} Date: Mon, 4 Nov 2024 12:53:55 +0100 Subject: [PATCH 30/44] chore: remove connected param in the last step --- .github/pull_request_template.md | 4 ---- .../providers/(set-up-provider)/launch-scan/page.tsx | 5 ++--- components/providers/workflow/forms/launch-scan-form.tsx | 7 ++++--- .../providers/workflow/forms/test-connection-form.tsx | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) 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/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx b/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx index 0fd9edb08c..3e88a5ea46 100644 --- a/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/launch-scan/page.tsx @@ -5,12 +5,11 @@ import { getProvider } from "@/actions/providers"; import { LaunchScanForm } from "@/components/providers/workflow/forms"; interface Props { - searchParams: { type: string; id: string; connected: boolean }; + searchParams: { type: string; id: string }; } export default async function LaunchScanPage({ searchParams }: Props) { const providerId = searchParams.id; - const connectedSearchParam = searchParams.connected; if (!providerId) { redirect("/providers/connect-account"); @@ -23,7 +22,7 @@ export default async function LaunchScanPage({ searchParams }: Props) { const isConnected = providerData?.data?.attributes?.connection?.connected; - if (!isConnected || connectedSearchParam !== isConnected.toString()) { + if (!isConnected) { redirect("/providers/connect-account"); } diff --git a/components/providers/workflow/forms/launch-scan-form.tsx b/components/providers/workflow/forms/launch-scan-form.tsx index 478cef5f62..8f335afbf9 100644 --- a/components/providers/workflow/forms/launch-scan-form.tsx +++ b/components/providers/workflow/forms/launch-scan-form.tsx @@ -16,7 +16,7 @@ import { ProviderInfo } from "../../provider-info"; type FormValues = z.infer>; interface LaunchScanFormProps { - searchParams: { type: string; id: string; connected: boolean }; + searchParams: { type: string; id: string }; providerData: { data: { type: string; @@ -32,7 +32,6 @@ export const LaunchScanForm = ({ }: LaunchScanFormProps) => { const providerType = searchParams.type; const providerId = searchParams.id; - const connected = searchParams.connected; // const [apiErrorMessage, setApiErrorMessage] = useState(null); @@ -53,10 +52,12 @@ export const LaunchScanForm = ({ const isLoading = form.formState.isSubmitting; + const isConnected = providerData.data.attributes.connection.connected; + const onSubmitClient = async (values: FormValues) => { console.log({ values }, "values from test connection form"); - if (connected) { + if (isConnected) { console.log("connected"); } else { console.log("not connected"); diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index 5a0bc394c6..d18e926138 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -85,7 +85,7 @@ export const TestConnectionForm = ({ if (connected) { router.push( - `/providers/launch-scan?type=${providerType}&id=${providerId}&connected=${connected}`, + `/providers/launch-scan?type=${providerType}&id=${providerId}`, ); } } From 9a9481a88e7c095d4766c885c62c338300c4b9bf Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 4 Nov 2024 13:21:45 +0100 Subject: [PATCH 31/44] chore: set buttons for start scan now or schedule it --- components/icons/Icons.tsx | 129 ++++-------------- .../workflow/forms/launch-scan-form.tsx | 21 ++- components/providers/workflow/workflow.tsx | 2 +- 3 files changed, 44 insertions(+), 108 deletions(-) diff --git a/components/icons/Icons.tsx b/components/icons/Icons.tsx index 6d886cf788..7b7ccd7cda 100644 --- a/components/icons/Icons.tsx +++ b/components/icons/Icons.tsx @@ -201,108 +201,6 @@ export const VerticalDotsIcon: React.FC = ({ ); -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/workflow/forms/launch-scan-form.tsx b/components/providers/workflow/forms/launch-scan-form.tsx index 8f335afbf9..fa44457dbf 100644 --- a/components/providers/workflow/forms/launch-scan-form.tsx +++ b/components/providers/workflow/forms/launch-scan-form.tsx @@ -4,7 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { SaveIcon } from "@/components/icons"; +import { RocketIcon, ScheduleIcon } from "@/components/icons"; // import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; @@ -48,8 +48,6 @@ export const LaunchScanForm = ({ }, }); - console.log({ providerData }); - const isLoading = form.formState.isSubmitting; const isConnected = providerData.data.attributes.connection.connected; @@ -103,9 +101,22 @@ export const LaunchScanForm = ({ color="action" size="lg" isLoading={isLoading} - startContent={!isLoading && } + startContent={!isLoading && } + isDisabled={true} > - {isLoading ? <>Loading : Launch scan} + Schedule + + } + > + {isLoading ? <>Loading : Start now}
diff --git a/components/providers/workflow/workflow.tsx b/components/providers/workflow/workflow.tsx index 516ba4e58b..da5fa18f99 100644 --- a/components/providers/workflow/workflow.tsx +++ b/components/providers/workflow/workflow.tsx @@ -67,7 +67,7 @@ export const Workflow = () => { From e04ba94aced3acb1f2c8449035ba0e764fc41394 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 4 Nov 2024 13:37:10 +0100 Subject: [PATCH 32/44] chore: Button for Start scan now is working now as the last step in the workflow --- .../workflow/forms/launch-scan-form.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/components/providers/workflow/forms/launch-scan-form.tsx b/components/providers/workflow/forms/launch-scan-form.tsx index fa44457dbf..86325d437b 100644 --- a/components/providers/workflow/forms/launch-scan-form.tsx +++ b/components/providers/workflow/forms/launch-scan-form.tsx @@ -1,11 +1,13 @@ "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 { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { ProviderProps } from "@/types"; // Asegúrate de importar la interfaz correcta @@ -33,7 +35,8 @@ export const LaunchScanForm = ({ const providerType = searchParams.type; const providerId = searchParams.id; - // const [apiErrorMessage, setApiErrorMessage] = useState(null); + const [apiErrorMessage, setApiErrorMessage] = useState(null); + const router = useRouter(); const formSchema = launchScanFormSchema(); @@ -50,15 +53,36 @@ export const LaunchScanForm = ({ const isLoading = form.formState.isSubmitting; - const isConnected = providerData.data.attributes.connection.connected; - const onSubmitClient = async (values: FormValues) => { - console.log({ values }, "values from test connection form"); + const formData = new FormData(); + formData.append("providerId", values.providerId); - if (isConnected) { - console.log("connected"); - } else { - console.log("not connected"); + // 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.", + }); } }; @@ -77,11 +101,11 @@ export const LaunchScanForm = ({
- {/* {apiErrorMessage && ( + {apiErrorMessage && (
-

{`Provider ID ${apiErrorMessage.toLowerCase()}. Please check and try again.`}

+

{apiErrorMessage.toLowerCase()}

- )} */} + )} Date: Tue, 5 Nov 2024 07:07:53 +0100 Subject: [PATCH 33/44] chore: hidden credentials inputs for cloud providers --- .../add-credentials/page.tsx | 10 +++--- .../test-connection/page.tsx | 19 ++++++++-- components/findings/table/column-findings.tsx | 2 +- .../workflow/forms/connect-account-form.tsx | 4 +-- .../workflow/forms/test-connection-form.tsx | 35 +++++++++++++++++-- .../workflow/forms/via-credentials-form.tsx | 4 +-- .../via-credentials/aws-credentials-form.tsx | 6 ++-- .../azure-credentials-form.tsx | 2 +- .../via-credentials/gcp-credentials-form.tsx | 4 +-- 9 files changed, 65 insertions(+), 21 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index 83ae37a99a..b5fda55e11 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -4,21 +4,21 @@ import React from "react"; import { ViaCredentialsForm } from "@/components/providers/workflow/forms"; interface Props { - searchParams: { provider: string; id: string; via?: string }; + searchParams: { type: string; id: string; via?: string }; } export default function AddCredentialsPage({ searchParams }: Props) { if ( - !searchParams.provider || + !searchParams.type || !searchParams.id || - (searchParams.provider === "aws" && !searchParams.via) + (searchParams.type === "aws" && !searchParams.via) ) { redirect("/providers/connect-account"); } const useCredentialsForm = - (searchParams.provider === "aws" && searchParams.via === "credentials") || - (searchParams.provider !== "aws" && !searchParams.via); + (searchParams.type === "aws" && searchParams.via === "credentials") || + (searchParams.type !== "aws" && !searchParams.via); 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 index e3bdb29928..dcf2f2766d 100644 --- a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -1,16 +1,29 @@ import { redirect } from "next/navigation"; import React from "react"; +import { getProvider } from "@/actions/providers"; import { TestConnectionForm } from "@/components/providers/workflow/forms"; interface Props { searchParams: { type: string; id: string }; } -export default function TestConnectionPage({ searchParams }: Props) { - if (!searchParams.id) { +export default async function TestConnectionPage({ searchParams }: Props) { + const providerId = searchParams.id; + + if (!providerId) { redirect("/providers/connect-account"); } - return ; + const formData = new FormData(); + formData.append("id", providerId); + + const providerData = await getProvider(formData); + + return ( + + ); } diff --git a/components/findings/table/column-findings.tsx b/components/findings/table/column-findings.tsx index 5ccfee3d23..9483ee9b86 100644 --- a/components/findings/table/column-findings.tsx +++ b/components/findings/table/column-findings.tsx @@ -88,7 +88,7 @@ export const ColumnFindings: ColumnDef[] = [ }, { accessorKey: "status", - header: "Scan Status", + header: "Status", cell: ({ row }) => { const { attributes: { status }, diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 53e4cc24ef..1c0430bf41 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -93,13 +93,13 @@ export const ConnectAccountForm = () => { } else { const { id, - attributes: { provider }, + attributes: { provider: providerType }, } = data.data; const credentialsParam = values.awsCredentialsType ? `&via=${values.awsCredentialsType}` : ""; router.push( - `/providers/add-credentials?provider=${provider}&id=${id}${credentialsParam}`, + `/providers/add-credentials?type=${providerType}&id=${id}${credentialsParam}`, ); } }; diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index d18e926138..499b55ede8 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -16,12 +16,27 @@ import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; 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; + attributes: { + connection: { + connected: boolean; + }; + provider: "aws" | "azure" | "gcp" | "kubernetes"; + alias: string; + }; + }; + }; }) => { const { toast } = useToast(); const router = useRouter(); @@ -68,7 +83,6 @@ export const TestConnectionForm = ({ } }); } else { - console.log({ data: data.data.id }, "success"); const taskId = data.data.id; setApiErrorMessage(null); @@ -128,6 +142,12 @@ export const TestConnectionForm = ({
)} + +
@@ -142,6 +162,17 @@ export const TestConnectionForm = ({ /> Back to providers + ) : connectionStatus?.error ? ( + + + Handle credentials + ) : ( } > - {isLoading ? <>Loading : Connect account} + {isLoading ? <>Loading : Test connection} )}
diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index fb2507e304..56a9161ea0 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -38,12 +38,12 @@ type FormType = CredentialsFormSchema & export const ViaCredentialsForm = ({ searchParams, }: { - searchParams: { provider: string; id: string }; + searchParams: { type: string; id: string }; }) => { const router = useRouter(); const { toast } = useToast(); - const providerType = searchParams.provider; + const providerType = searchParams.type; const providerId = searchParams.id; const formSchema = addCredentialsFormSchema(providerType); diff --git a/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx b/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx index b35b08f93c..a024d138ff 100644 --- a/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials/aws-credentials-form.tsx @@ -21,7 +21,7 @@ export const AWScredentialsForm = ({ Date: Tue, 5 Nov 2024 16:19:07 +0100 Subject: [PATCH 34/44] feat: aws providers can be added via role --- actions/providers/providers.ts | 35 ++++- .../add-credentials/page.tsx | 9 +- components/providers/workflow/forms/index.ts | 1 + .../radio-group-aws-via-credentials-form.tsx | 15 +-- .../workflow/forms/via-credentials-form.tsx | 17 +-- .../workflow/forms/via-role-form.tsx | 125 ++++++++++++++++++ .../workflow/forms/via-role/aws-role-form.tsx | 105 +++++++++++++++ .../workflow/forms/via-role/index.ts | 1 + types/components.ts | 10 ++ types/formSchemas.ts | 18 +++ 10 files changed, 300 insertions(+), 36 deletions(-) create mode 100644 components/providers/workflow/forms/via-role-form.tsx create mode 100644 components/providers/workflow/forms/via-role/aws-role-form.tsx create mode 100644 components/providers/workflow/forms/via-role/index.ts diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index a1704c4fea..3a651783df 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -158,27 +158,50 @@ export const addCredentialsProvider = async (formData: FormData) => { 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") { - 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, - }; + 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"), }; @@ -188,7 +211,7 @@ export const addCredentialsProvider = async (formData: FormData) => { data: { type: "ProviderSecret", attributes: { - secret_type: "static", + secret_type: secretType, secret, name: secretName, }, diff --git a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index b5fda55e11..6a525990fa 100644 --- a/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -1,7 +1,10 @@ import { redirect } from "next/navigation"; import React from "react"; -import { ViaCredentialsForm } from "@/components/providers/workflow/forms"; +import { + ViaCredentialsForm, + ViaRoleForm, +} from "@/components/providers/workflow/forms"; interface Props { searchParams: { type: string; id: string; via?: string }; @@ -20,9 +23,13 @@ export default function AddCredentialsPage({ searchParams }: Props) { (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/components/providers/workflow/forms/index.ts b/components/providers/workflow/forms/index.ts index cf0417ed09..be1bb3277d 100644 --- a/components/providers/workflow/forms/index.ts +++ b/components/providers/workflow/forms/index.ts @@ -3,3 +3,4 @@ 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/radio-group-aws-via-credentials-form.tsx b/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx index a0a04f551d..7a870c79ff 100644 --- a/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx +++ b/components/providers/workflow/forms/radio-group-aws-via-credentials-form.tsx @@ -34,20 +34,9 @@ export const RadioGroupAWSViaCredentialsForm = ({ >
Using IAM Role - +
- Connect via CloudFormation -
-
- -
- Connect via Terraform + Connect assuming IAM Role
diff --git a/components/providers/workflow/forms/via-credentials-form.tsx b/components/providers/workflow/forms/via-credentials-form.tsx index 56a9161ea0..176ed40a00 100644 --- a/components/providers/workflow/forms/via-credentials-form.tsx +++ b/components/providers/workflow/forms/via-credentials-form.tsx @@ -8,7 +8,7 @@ import * as z from "zod"; import { addCredentialsProvider } from "@/actions/providers/providers"; import { useToast } from "@/components/ui"; -import { CustomButton, CustomInput } from "@/components/ui/custom"; +import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; import { addCredentialsFormSchema, @@ -50,7 +50,6 @@ export const ViaCredentialsForm = ({ const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { - secretName: "", providerId, providerType, ...(providerType === "aws" @@ -188,20 +187,6 @@ export const ViaCredentialsForm = ({ /> )} - Name (Optional) - -
{ + 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 ( +
+ + + + + {providerType === "aws" && ( + } + /> + )} + +
+ } + > + {isLoading ? <>Loading : Save} + +
+ + + ); +}; 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/types/components.ts b/types/components.ts index 9c84ce65ab..b6c4b59da2 100644 --- a/types/components.ts +++ b/types/components.ts @@ -34,6 +34,16 @@ export type AWSCredentials = { 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; diff --git a/types/formSchemas.ts b/types/formSchemas.ts index bd7c0b6585..a90ffc8c02 100644 --- a/types/formSchemas.ts +++ b/types/formSchemas.ts @@ -104,6 +104,24 @@ export const addCredentialsFormSchema = (providerType: string) => : {}), }); +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(), }); From f96777bcf9d6cc2f39c31f79c736eea2081f652f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Tue, 5 Nov 2024 16:35:48 +0100 Subject: [PATCH 35/44] chore: handle data when executing the request --- .../(set-up-provider)/test-connection/page.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx index dcf2f2766d..4a3f3a7f15 100644 --- a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; -import React from "react"; +import React, { Suspense } from "react"; import { getProvider } from "@/actions/providers"; import { TestConnectionForm } from "@/components/providers/workflow/forms"; @@ -15,8 +15,20 @@ export default async function TestConnectionPage({ searchParams }: Props) { redirect("/providers/connect-account"); } + return ( + Loading...

}> + +
+ ); +} + +async function SSRTestConnection({ + searchParams, +}: { + searchParams: { type: string; id: string }; +}) { const formData = new FormData(); - formData.append("id", providerId); + formData.append("id", searchParams.id); const providerData = await getProvider(formData); From 76c6065a80edf936060a4a6560e2563744a080ef Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 07:45:57 +0100 Subject: [PATCH 36/44] fix: avoid app crashed when there is no data to render --- .../test-connection/page.tsx | 3 +++ lib/helper.ts | 21 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx index 4a3f3a7f15..13cac50f6d 100644 --- a/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx +++ b/app/(prowler)/providers/(set-up-provider)/test-connection/page.tsx @@ -31,6 +31,9 @@ async function SSRTestConnection({ formData.append("id", searchParams.id); const providerData = await getProvider(formData); + if (providerData.errors) { + redirect("/providers/connect-account"); + } return ( 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); From e444e39fd08dde083c7cd9983002b40aea308e5d Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 08:29:39 +0100 Subject: [PATCH 37/44] feat: add helper function to monitor task state during execution --- .../table/data-table-row-actions.tsx | 10 ++++- .../workflow/forms/test-connection-form.tsx | 42 ++++++++++++------- lib/helper.ts | 38 +++++++++++++++++ 3 files changed, 74 insertions(+), 16 deletions(-) diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 127e829c4a..3e541751da 100644 --- a/components/providers/table/data-table-row-actions.tsx +++ b/components/providers/table/data-table-row-actions.tsx @@ -7,6 +7,7 @@ import { DropdownMenu, DropdownSection, DropdownTrigger, + Link, } from "@nextui-org/react"; import { AddNoteBulkIcon, @@ -35,6 +36,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 { type: string }).type; const providerAlias = (row.original as any).attributes?.alias; return ( <> @@ -79,7 +81,13 @@ export function DataTableRowActions({ textValue="Check Connection" startContent={} > - {/* TODO: add the provider type to the search params */} + + + Test Connection + + Test connection
-
- Please check the provider connection -
+

+ Ensure all required credentials and configurations are completed + accurately. A successful connection will enable the option to + initiate a scan in the following step. +

{apiErrorMessage && ( diff --git a/lib/helper.ts b/lib/helper.ts index 2777e9d344..8e9eb843e6 100644 --- a/lib/helper.ts +++ b/lib/helper.ts @@ -1,7 +1,45 @@ +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) { + 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 function createDict(type: string, data: any) { const includedField = data?.included?.filter( From 44b02088461d9b86367747a00193924854502ae0 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 08:47:56 +0100 Subject: [PATCH 38/44] chore: handle API error from test connection --- .../providers/workflow/forms/test-connection-form.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index 3076ecb658..2c0d6375b6 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -91,22 +91,25 @@ export const TestConnectionForm = ({ const taskResult = await checkTaskStatus(taskId); if (taskResult.completed) { - // If the task is completed, fetch the final task data const task = await getTask(taskId); - const connected = task.data.attributes.result.connected; + const { connected, error } = task.data.attributes.result; setConnectionStatus({ connected, - error: null, + 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 { - // If the task failed, display the error message setConnectionStatus({ connected: false, error: taskResult.error || "Unknown error", From c62ab62bf97b62170afd436fac985be7d327fcb9 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 09:34:18 +0100 Subject: [PATCH 39/44] feat: improve custom button and add prop to use it asLink when needed --- app/(prowler)/providers/page.tsx | 2 -- components/providers/add-provider.tsx | 1 + components/ui/custom/custom-button.tsx | 6 +++++- components/ui/headers/navigation-header.tsx | 20 +++++++++-------- components/ui/sidebar/sidebar-wrap.tsx | 24 ++++++++++----------- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index f80b59241e..177b403745 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -1,5 +1,4 @@ import { Spacer } from "@nextui-org/react"; -import Link from "next/link"; import { Suspense } from "react"; import { getProviders } from "@/actions/providers"; @@ -27,7 +26,6 @@ export default async function Providers({ - add provider diff --git a/components/providers/add-provider.tsx b/components/providers/add-provider.tsx index 01e85db980..c732d41007 100644 --- a/components/providers/add-provider.tsx +++ b/components/providers/add-provider.tsx @@ -7,6 +7,7 @@ export const AddProvider = () => { return (
; + asLink?: string; } export const CustomButton = React.forwardRef< @@ -73,11 +74,14 @@ export const CustomButton = React.forwardRef< isDisabled = false, isLoading = false, isIconOnly, + asLink, ...props }, ref, ) => (
From 05e3be418d41d1f6e4bc350ee077fcfeb1ce07fb Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 10:55:40 +0100 Subject: [PATCH 40/44] fix: the test connection button from actions in the providers table is working as expected now --- .../providers/table/data-table-row-actions.tsx | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 3e541751da..604863d7c2 100644 --- a/components/providers/table/data-table-row-actions.tsx +++ b/components/providers/table/data-table-row-actions.tsx @@ -7,7 +7,6 @@ import { DropdownMenu, DropdownSection, DropdownTrigger, - Link, } from "@nextui-org/react"; import { AddNoteBulkIcon, @@ -35,8 +34,9 @@ export function DataTableRowActions({ }: DataTableRowActionsProps) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); + console.log("row.original:", row.original); const providerId = (row.original as { id: string }).id; - const providerType = (row.original as { type: string }).type; + const providerType = (row.original as any).attributes?.provider; const providerAlias = (row.original as any).attributes?.alias; return ( <> @@ -76,18 +76,13 @@ export function DataTableRowActions({ > } > - - - Test Connection - - + Test Connection Date: Wed, 6 Nov 2024 11:03:12 +0100 Subject: [PATCH 41/44] chore: replace Link component to use it from NextJS and not from NextUI --- components/providers/table/data-table-row-actions.tsx | 1 - components/ui/custom/custom-button.tsx | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/providers/table/data-table-row-actions.tsx b/components/providers/table/data-table-row-actions.tsx index 604863d7c2..0642114bb4 100644 --- a/components/providers/table/data-table-row-actions.tsx +++ b/components/providers/table/data-table-row-actions.tsx @@ -34,7 +34,6 @@ export function DataTableRowActions({ }: DataTableRowActionsProps) { const [isEditOpen, setIsEditOpen] = useState(false); const [isDeleteOpen, setIsDeleteOpen] = useState(false); - console.log("row.original:", row.original); const providerId = (row.original as { id: string }).id; const providerType = (row.original as any).attributes?.provider; const providerAlias = (row.original as any).attributes?.alias; diff --git a/components/ui/custom/custom-button.tsx b/components/ui/custom/custom-button.tsx index 9c3e71b3f4..eeffd07783 100644 --- a/components/ui/custom/custom-button.tsx +++ b/components/ui/custom/custom-button.tsx @@ -1,6 +1,7 @@ -import { Button, CircularProgress, Link } from "@nextui-org/react"; +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"; From b11a33d3dab424236f888ee7fc405763decd66ad Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 13:46:03 +0100 Subject: [PATCH 42/44] feat: reset credentials for gcp, azure and kubernetes if test connection fail --- actions/providers/providers.ts | 22 ++++ .../workflow/forms/test-connection-form.tsx | 116 ++++++++++++++---- lib/helper.ts | 1 + 3 files changed, 113 insertions(+), 26 deletions(-) diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 3a651783df..cf079892b4 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -274,6 +274,28 @@ export const checkConnectionProvider = async (formData: FormData) => { } }; +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(); + revalidatePath("/providers"); + return parseStringify(data); + } catch (error) { + return { + error: getErrorMessage(error), + }; + } +}; + export const deleteProvider = async (formData: FormData) => { const session = await auth(); const keyServer = process.env.API_BASE_URL; diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index 2c0d6375b6..6341faaf34 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -8,9 +8,12 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { checkConnectionProvider } from "@/actions/providers/providers"; +import { + checkConnectionProvider, + deleteCredentials, +} from "@/actions/providers"; import { getTask } from "@/actions/task/tasks"; -import { SaveIcon } from "@/components/icons"; +import { CheckIcon, SaveIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; @@ -29,12 +32,23 @@ export const TestConnectionForm = ({ providerData: { data: { id: string; + type: string; attributes: { connection: { - connected: boolean; + 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; + }; }; }; }; @@ -43,13 +57,14 @@ export const TestConnectionForm = ({ 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), @@ -118,6 +133,41 @@ export const TestConnectionForm = ({ } }; + const onResetCredentials = async () => { + setIsResettingCredentials(true); + // Check if provider has no credentials + const providerSecretId = + providerData?.data?.relationships?.secret?.data?.id; + const hasNoCredentials = !providerSecretId; + console.log({ providerSecretId }, "providerSecretId"); + console.log({ hasNoCredentials }, "hasNoCredentials"); + + if (hasNoCredentials) { + // If no credentials, redirect to add credentials page + console.log("no credentials"); + // router.push( + // `/providers/add-credentials?type=${providerType}&id=${providerId}`, + // ); + return; + } + + // If has credentials, delete them first + try { + // This function will need to be implemented + await deleteCredentials(providerSecretId); + // After successful deletion, redirect to add credentials page + console.log("deleted credentials with success"); + router.push( + `/providers/add-credentials?type=${providerType}&id=${providerId}`, + ); + } catch (error) { + // Handle error appropriately + console.error("Failed to delete credentials:", error); + } finally { + setIsResettingCredentials(false); + } + }; + return (
{apiErrorMessage && ( -
+

{`Provider ID ${apiErrorMessage.toLowerCase()}. Please check and try again.`}

)} {connectionStatus && !connectionStatus.connected && ( -
-
- + <> +
+
+ +
+
+

+ {connectionStatus.error || "Unknown error"} +

+
-
-

- {connectionStatus.error || "Unknown error"} -

-
-
+

+ It seems there was an issue with your credentials. Please review + your credentials and try again. +

+ )} Back to providers ) : connectionStatus?.error ? ( - } + isDisabled={isResettingCredentials} > - - Handle credentials - + {isResettingCredentials ? ( + <>Loading + ) : ( + Reset credentials + )} + ) : ( Date: Wed, 6 Nov 2024 14:06:14 +0100 Subject: [PATCH 43/44] chore: remove unused console log --- .../workflow/forms/connect-account-form.tsx | 1 - .../workflow/forms/test-connection-form.tsx | 17 ++++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/components/providers/workflow/forms/connect-account-form.tsx b/components/providers/workflow/forms/connect-account-form.tsx index 1c0430bf41..9a0825ef66 100644 --- a/components/providers/workflow/forms/connect-account-form.tsx +++ b/components/providers/workflow/forms/connect-account-form.tsx @@ -49,7 +49,6 @@ export const ConnectAccountForm = () => { formValues.providerAlias = `${formValues.providerType}:${month}/${day}/${year}`; } - console.log({ formValues }); const formData = new FormData(); Object.entries(formValues).forEach( diff --git a/components/providers/workflow/forms/test-connection-form.tsx b/components/providers/workflow/forms/test-connection-form.tsx index 6341faaf34..6d7ca16dbe 100644 --- a/components/providers/workflow/forms/test-connection-form.tsx +++ b/components/providers/workflow/forms/test-connection-form.tsx @@ -135,33 +135,28 @@ export const TestConnectionForm = ({ const onResetCredentials = async () => { setIsResettingCredentials(true); - // Check if provider has no credentials + + // Check if provider the provider has no credentials const providerSecretId = providerData?.data?.relationships?.secret?.data?.id; const hasNoCredentials = !providerSecretId; - console.log({ providerSecretId }, "providerSecretId"); - console.log({ hasNoCredentials }, "hasNoCredentials"); if (hasNoCredentials) { // If no credentials, redirect to add credentials page - console.log("no credentials"); - // router.push( - // `/providers/add-credentials?type=${providerType}&id=${providerId}`, - // ); + router.push( + `/providers/add-credentials?type=${providerType}&id=${providerId}`, + ); return; } - // If has credentials, delete them first + // If provider has credentials, delete them first try { - // This function will need to be implemented await deleteCredentials(providerSecretId); // After successful deletion, redirect to add credentials page - console.log("deleted credentials with success"); router.push( `/providers/add-credentials?type=${providerType}&id=${providerId}`, ); } catch (error) { - // Handle error appropriately console.error("Failed to delete credentials:", error); } finally { setIsResettingCredentials(false); From 2448f9b0293530d51114d5317c6003b8d1bd3b89 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 6 Nov 2024 14:29:10 +0100 Subject: [PATCH 44/44] chore: build is working as expected --- app/(prowler)/findings/page.tsx | 4 +++- components/findings/table/data-table-row-actions.tsx | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) 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/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 ( <> {/*