Compare commits

...

5 Commits

Author SHA1 Message Date
HugoPBrito
7ae8ed0fe3 chore: resolve conflicts 2025-07-22 16:07:55 +02:00
HugoPBrito
40219d5f09 chore: add to changelog 2025-07-22 16:07:03 +02:00
HugoPBrito
638f44e6c9 chore: enhance label 2025-07-22 15:59:45 +02:00
HugoPBrito
6d94cd54ae fix: linter 2025-07-22 15:59:44 +02:00
HugoPBrito
186851e264 feat: add select auth method form 2025-07-21 10:34:46 +02:00
13 changed files with 363 additions and 9 deletions

View File

@@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Navigation link in Scans view to access Compliance Overview [(#8251)](https://github.com/prowler-cloud/prowler/pull/8251)
- Status column for findings table in the Compliance Detail view [(#8244)](https://github.com/prowler-cloud/prowler/pull/8244)
- Allow to restrict routes access based on user permissions [(#8287)](https://github.com/prowler-cloud/prowler/pull/8287)
- Support for Service Principal-only and Service Principal + User authentication flows for M365 provider [(#8332)](https://github.com/prowler-cloud/prowler/pull/8332)
- Max character limit validation for Scan label [(#8319)](https://github.com/prowler-cloud/prowler/pull/8319)
### Security

View File

@@ -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,18 @@ export default function AddCredentialsPage({ searchParams }: Props) {
<SelectViaGCP initialVia={searchParams.via} />
)}
{searchParams.type === "m365" && !searchParams.via && (
<SelectViaM365 initialVia={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")) && (
<AddViaCredentialsForm searchParams={searchParams} />
)}

View File

@@ -20,7 +20,9 @@ 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 && (
<CredentialsUpdateInfo
providerType={searchParams.type}
@@ -30,7 +32,12 @@ export default function UpdateCredentialsPage({ searchParams }: Props) {
{((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")) && (
<UpdateViaCredentialsForm searchParams={searchParams} />
)}

View File

@@ -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 <SelectViaGCP initialVia={initialVia} />;
}
if (providerType === "m365") {
return <SelectViaM365 initialVia={initialVia} />;
}
return null;
};

View File

@@ -24,9 +24,10 @@ import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credenti
import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type";
import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
import { M365ServicePrincipalForm } from "./select-credentials-type/m365/credentials-type";
import { M365ServicePrincipalUserForm } from "./select-credentials-type/m365/credentials-type/m365-service-principal-user-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";
type BaseCredentialsFormProps = {
providerType: ProviderType;
@@ -97,11 +98,18 @@ export const BaseCredentialsForm = ({
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") === "service-principal-user" && (
<M365ServicePrincipalUserForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "m365" &&
searchParamsObj.get("via") !== "service-principal-user" && (
<M365ServicePrincipalForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "gcp" &&
searchParamsObj.get("via") === "service-account" && (
<GCPServiceAccountKeyForm

View File

@@ -0,0 +1,2 @@
export { M365ServicePrincipalForm } from "./m365-service-principal-form";
export { M365ServicePrincipalUserForm } from "./m365-service-principal-user-form";

View File

@@ -0,0 +1,67 @@
import { Control } 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,
}: {
control: Control<M365Credentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md font-bold leading-9 text-default-foreground">
Application/Service Principal
</div>
<div className="text-sm text-default-500">
Please provide the Application/Service Principal information for your
Microsoft 365 tenant.
</div>
</div>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_ID}
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.CLIENT_ID]
}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_SECRET}
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.CLIENT_SECRET]
}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.TENANT_ID}
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.TENANT_ID]
}
/>
</>
);
};

View File

@@ -0,0 +1,109 @@
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<M365Credentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md font-bold leading-9 text-default-foreground">
Connect using Application/Service Principal and User Credentials
</div>
<div className="text-sm text-default-500">
Connect using Service Principal credentials combined with user
authentication.
</div>
</div>
<span className="text-xs font-bold text-default-500">
Service Principal Information
</span>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_ID}
type="text"
label="Client ID"
labelPlacement="inside"
placeholder="Enter the Client ID"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.CLIENT_ID]
}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.CLIENT_SECRET}
type="password"
label="Client Secret"
labelPlacement="inside"
placeholder="Enter the Client Secret"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.CLIENT_SECRET]
}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.TENANT_ID}
type="text"
label="Tenant ID"
labelPlacement="inside"
placeholder="Enter the Tenant ID"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.TENANT_ID]
}
/>
<span className="text-xs font-bold text-default-500">
User Credentials
</span>
<CustomInput
control={control}
name={ProviderCredentialFields.USER}
type="text"
label="User"
labelPlacement="inside"
placeholder="Enter the User (e.g., user@company.onmicrosoft.com)"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors[ProviderCredentialFields.USER]}
/>
<CustomInput
control={control}
name={ProviderCredentialFields.PASSWORD}
type="password"
label="Password"
labelPlacement="inside"
placeholder="Enter the Password"
variant="bordered"
isRequired
isInvalid={
!!control._formState.errors[ProviderCredentialFields.PASSWORD]
}
/>
<div className="flex items-center rounded-lg border border-system-warning bg-system-warning-medium p-2 text-sm dark:text-default-300">
<InfoIcon className="mr-2 inline h-4 w-4 flex-shrink-0" />
<p className="text-xs font-extrabold">
By September 2025, User Authentication will be deprecated.
</p>
</div>
</>
);
};

View File

@@ -0,0 +1,3 @@
export * from "./credentials-type";
export { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form";
export { SelectViaM365 } from "./select-via-m365";

View File

@@ -0,0 +1,74 @@
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<any>;
isInvalid?: boolean;
errorMessage?: string;
onChange?: (value: string) => void;
}
export const RadioGroupM365ViaCredentialsTypeForm = ({
control,
isInvalid,
errorMessage,
onChange,
}: RadioGroupM365ViaCredentialsFormProps) => {
return (
<Controller
name="m365CredentialsType"
control={control}
render={({ field }) => (
<>
<RadioGroup
className="flex flex-wrap"
isInvalid={isInvalid}
{...field}
value={field.value || ""}
onValueChange={(value) => {
field.onChange(value);
if (onChange) {
onChange(value);
}
}}
>
<div className="flex flex-col gap-4">
<span className="text-sm text-default-500">
Application Authentication
</span>
<CustomRadio
description="Connect using Service Principal credentials only"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">Service Principal</span>
</div>
</CustomRadio>
<span className="text-sm text-default-500">
Application + User Authentication
</span>
<CustomRadio
description="Connect using Service Principal + User credentials"
value="service-principal-user"
>
<div className="flex items-center">
<span className="ml-2">
Service Principal + User Credentials
</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-system-error dark:text-system-error">
{errorMessage}
</FormMessage>
)}
</>
)}
/>
);
};

View File

@@ -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 (
<Form {...form}>
<RadioGroupM365ViaCredentialsTypeForm
control={form.control}
isInvalid={!!form.formState.errors.m365CredentialsType}
errorMessage={form.formState.errors.m365CredentialsType?.message}
onChange={handleSelectionChange}
/>
</Form>
);
};

View File

@@ -9,6 +9,7 @@ import { PROVIDER_CREDENTIALS_ERROR_MAPPING } from "@/lib/error-mappings";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import {
addCredentialsFormSchema,
addCredentialsM365UserFormSchema,
addCredentialsRoleFormSchema,
addCredentialsServiceAccountFormSchema,
ProviderType,
@@ -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);
};

View File

@@ -69,6 +69,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"], {
@@ -257,6 +263,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),