diff --git a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index b867e919d0..b0e83b9257 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -9,6 +9,7 @@ import { AddViaServiceAccountForm, SelectViaGCP, } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; +import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365"; import { ProviderType } from "@/types/providers"; interface Props { @@ -26,9 +27,15 @@ export default function AddCredentialsPage({ searchParams }: Props) { )} + {searchParams.type === "m365" && !searchParams.via && ( + + )} + {((searchParams.type === "aws" && searchParams.via === "credentials") || (searchParams.type === "gcp" && searchParams.via === "credentials") || - (searchParams.type !== "aws" && searchParams.type !== "gcp")) && ( + (searchParams.type === "m365" && searchParams.via === "credentials") || + (searchParams.type === "m365" && searchParams.via === "service-principal-user") || + (searchParams.type !== "aws" && searchParams.type !== "gcp" && searchParams.type !== "m365")) && ( )} diff --git a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx index 185a107244..6d59149b82 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/update-credentials/page.tsx @@ -20,7 +20,7 @@ interface Props { export default function UpdateCredentialsPage({ searchParams }: Props) { return ( <> - {(searchParams.type === "aws" || searchParams.type === "gcp") && + {(searchParams.type === "aws" || searchParams.type === "gcp" || searchParams.type === "m365") && !searchParams.via && ( )} diff --git a/ui/components/providers/credentials-update-info.tsx b/ui/components/providers/credentials-update-info.tsx index 2715a1ea6b..8014c0bb39 100644 --- a/ui/components/providers/credentials-update-info.tsx +++ b/ui/components/providers/credentials-update-info.tsx @@ -2,6 +2,7 @@ import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; +import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365"; import { ProviderType } from "@/types/providers"; interface UpdateCredentialsInfoProps { @@ -20,6 +21,9 @@ export const CredentialsUpdateInfo = ({ if (providerType === "gcp") { return ; } + if (providerType === "m365") { + return ; + } return null; }; diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 39ab8ff6b3..47a3806ad7 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -26,7 +26,8 @@ import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credent import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form"; import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; -import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; +import { M365ServicePrincipalForm } from "./select-credentials-type/m365/credentials-type"; +import { M365ServicePrincipalUserForm } from "./select-credentials-type/m365/credentials-type/m365-service-principal-user-form"; type BaseCredentialsFormProps = { providerType: ProviderType; @@ -97,8 +98,13 @@ export const BaseCredentialsForm = ({ control={form.control as unknown as Control} /> )} - {providerType === "m365" && ( - } + /> + )} + {providerType === "m365" && searchParamsObj.get("via") !== "service-principal-user" && ( + } /> )} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts new file mode 100644 index 0000000000..9ee7a04604 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts @@ -0,0 +1,2 @@ +export { M365ServicePrincipalForm } from "./m365-service-principal-form"; +export { M365ServicePrincipalUserForm } from "./m365-service-principal-user-form"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-form.tsx new file mode 100644 index 0000000000..231824924e --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-form.tsx @@ -0,0 +1,132 @@ +import Link from "next/link"; +import { Control, UseFormSetValue, useWatch } from "react-hook-form"; + +import { CustomInput } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { M365Credentials } from "@/types"; + +export const M365ServicePrincipalForm = ({ + control, + setValue, +}: { + control: Control; + setValue: UseFormSetValue; + externalId: string; +}) => { + const credentialsType = useWatch({ + control, + name: ProviderCredentialFields.CREDENTIALS_TYPE, + defaultValue: "m365-service-principal", + }); + + return ( + <> +
+
+ Connect using Application/Service Principal +
+
+ Please provide the information for your Microsoft 365 Service Principal. +
+
+ + Authentication + + + + {credentialsType === "m365-service-principal" && ( + <> + + + + + )} + + {credentialsType === "m365-user-credentials" && ( + <> + + + + + + + )} + + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-user-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-user-form.tsx new file mode 100644 index 0000000000..ee298ce3bb --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-service-principal-user-form.tsx @@ -0,0 +1,122 @@ +import Link from "next/link"; +import { Control } from "react-hook-form"; + +import { InfoIcon } from "@/components/icons"; +import { CustomInput } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { M365Credentials } from "@/types"; + +export const M365ServicePrincipalUserForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Service Principal + User Credentials +
+
+ Connect using Service Principal credentials combined with user authentication for PowerShell modules. +
+
+ +
+ +
+

This method provides:

+
    +
  • Service Principal authentication for Microsoft Graph APIs
  • +
  • User credentials for PowerShell modules access
  • +
  • Complete M365 service coverage
  • +
+
+
+ + Service Principal Information + + + + + + + + User Credentials for PowerShell + + + + + +
+ +

+ By September 2025, User Authentication will be deprecated. +

+
+ +

+ For more information on setting up authentication, see the{" "} + + M365 setup documentation + + . +

+ + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts new file mode 100644 index 0000000000..e5283a36b2 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts @@ -0,0 +1,3 @@ +export { SelectViaM365 } from "./select-via-m365"; +export { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form"; +export * from "./credentials-type"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx new file mode 100644 index 0000000000..61ecd28122 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx @@ -0,0 +1,70 @@ +import { RadioGroup } from "@nextui-org/react"; +import { Control, Controller } from "react-hook-form"; + +import { CustomRadio } from "@/components/ui/custom"; +import { FormMessage } from "@/components/ui/form"; + +interface RadioGroupM365ViaCredentialsFormProps { + control: Control; + isInvalid?: boolean; + errorMessage?: string; + onChange?: (value: string) => void; +} + +export const RadioGroupM365ViaCredentialsTypeForm = ({ + control, + isInvalid, + errorMessage, + onChange, +}: RadioGroupM365ViaCredentialsFormProps) => { + return ( + ( + <> + { + field.onChange(value); + if (onChange) { + onChange(value); + } + }} + > +
+ Application Authentication + +
+ Service Principal +
+
+ + Application + User Authentication + + +
+ Service Principal + User Credentials +
+
+
+
+ {errorMessage && ( + + {errorMessage} + + )} + + )} + /> + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx new file mode 100644 index 0000000000..020e4ed0e1 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; + +import { Form } from "@/components/ui/form"; + +import { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form"; + +interface SelectViaM365Props { + initialVia?: string; +} + +export const SelectViaM365 = ({ initialVia }: SelectViaM365Props) => { + const router = useRouter(); + const form = useForm({ + defaultValues: { + m365CredentialsType: initialVia || "", + }, + }); + + const handleSelectionChange = (value: string) => { + const url = new URL(window.location.href); + url.searchParams.set("via", value); + router.push(url.toString()); + }; + + return ( +
+ + + ); +}; diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index df6ffa9517..fc0e89a48d 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -11,6 +11,7 @@ import { addCredentialsFormSchema, addCredentialsRoleFormSchema, addCredentialsServiceAccountFormSchema, + addCredentialsM365UserFormSchema, ProviderType, } from "@/types"; @@ -46,6 +47,9 @@ export const useCredentialsForm = ({ if (providerType === "gcp" && via === "service-account") { return addCredentialsServiceAccountFormSchema(providerType); } + if (providerType === "m365" && via === "service-principal-user") { + return addCredentialsM365UserFormSchema(); + } return addCredentialsFormSchema(providerType); }; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index b79c55e077..cb5b718ba8 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -66,6 +66,12 @@ export const awsCredentialsTypeSchema = z.object({ }), }); +export const m365CredentialsTypeSchema = z.object({ + m365CredentialsType: z.string().min(1, { + message: "Please select the type of credentials you want to use", + }), +}); + export const addProviderFormSchema = z .object({ providerType: z.enum(["aws", "azure", "gcp", "kubernetes", "m365"], { @@ -254,6 +260,27 @@ export const addCredentialsServiceAccountFormSchema = ( [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), }); +export const addCredentialsM365UserFormSchema = () => + z.object({ + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), + [ProviderCredentialFields.CLIENT_ID]: z + .string() + .nonempty("Client ID is required"), + [ProviderCredentialFields.CLIENT_SECRET]: z + .string() + .nonempty("Client Secret is required"), + [ProviderCredentialFields.TENANT_ID]: z + .string() + .nonempty("Tenant ID is required"), + [ProviderCredentialFields.USER]: z + .string() + .nonempty("User is required for this authentication method"), + [ProviderCredentialFields.PASSWORD]: z + .string() + .nonempty("Password is required for this authentication method"), + }); + export const testConnectionFormSchema = z.object({ [ProviderCredentialFields.PROVIDER_ID]: z.string(), runOnce: z.boolean().default(false),