mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-31 21:27:28 +00:00
Compare commits
10 Commits
PROWLER-XX
...
feat/PRWLR
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46bbbaaeb | ||
|
|
fd74850cfd | ||
|
|
579ae0538b | ||
|
|
81ec745af8 | ||
|
|
df856e30d9 | ||
|
|
9301702421 | ||
|
|
2a2724e4b6 | ||
|
|
461be92fb0 | ||
|
|
6ea9d006c4 | ||
|
|
40e959cd2f |
163
ui/AGENTS.md
163
ui/AGENTS.md
@@ -824,6 +824,169 @@ npm run start # Start production server
|
||||
- Network tab for API debugging
|
||||
- Lighthouse for performance analysis
|
||||
|
||||
## Code Spacing and Logical Unit Separation Guidelines
|
||||
|
||||
Proper spacing improves code readability and establishes clear boundaries between distinct operations.
|
||||
|
||||
### Rule 1: Separate Logical Units with Blank Lines
|
||||
|
||||
Add a blank line between distinct logical operations or phases in a function.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Clear separation between units
|
||||
async selectCredentialsType(type: "role" | "credentials"): Promise<void> {
|
||||
// Unit 1: Verify preconditions
|
||||
await expect(this.page).toHaveURL(/\/providers\/add-credentials/)
|
||||
|
||||
// Unit 2: Perform action
|
||||
if (type === "role") {
|
||||
await this.roleRadio.click({ force: true });
|
||||
} else {
|
||||
await this.staticCredentialsRadio.click({ force: true });
|
||||
}
|
||||
|
||||
// Unit 3: Verify result
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
|
||||
// ❌ BAD - No separation makes it hard to read
|
||||
async selectCredentialsType(type: "role" | "credentials"): Promise<void> {
|
||||
await expect(this.page).toHaveURL(/\/providers\/add-credentials/)
|
||||
if (type === "role") {
|
||||
await this.roleRadio.click({ force: true });
|
||||
} else {
|
||||
await this.staticCredentialsRadio.click({ force: true });
|
||||
}
|
||||
await this.waitForPageLoad();
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 2: Group Related Statements Together
|
||||
|
||||
Keep statements that belong to the same logical unit without blank lines.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Related statements grouped
|
||||
if (type === "role") {
|
||||
await this.roleRadio.click({ force: true });
|
||||
await this.waitForSelection();
|
||||
} else {
|
||||
await this.staticCredentialsRadio.click({ force: true });
|
||||
await this.waitForSelection();
|
||||
}
|
||||
|
||||
// ❌ BAD - Unnecessary spacing within same unit
|
||||
if (type === "role") {
|
||||
await this.roleRadio.click({ force: true });
|
||||
|
||||
await this.waitForSelection();
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 3: Separate Sequential Phases in Functions
|
||||
|
||||
Use blank lines to distinguish between:
|
||||
- **Assertion/Verification** phase (checks and expects)
|
||||
- **Action** phase (clicks, inputs, navigation)
|
||||
- **Wait/Confirmation** phase (waits and final assertions)
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
async navigateToAddCredentials(provider: string): Promise<void> {
|
||||
// Verification: Check current state
|
||||
await expect(this.page).toHaveURL(/\/providers/)
|
||||
|
||||
// Action: Navigate to add credentials
|
||||
await this.addCredentialsButton.click();
|
||||
await this.selectProvider(provider);
|
||||
|
||||
// Confirmation: Verify arrival
|
||||
await this.waitForPageLoad();
|
||||
await expect(this.page).toHaveURL(/\/providers\/add-credentials/)
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 4: One Blank Line Between Independent Operations
|
||||
|
||||
When two operations don't directly depend on each other or serve different purposes, separate them with a blank line.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Different concerns separated
|
||||
await this.fillPasswordField(password);
|
||||
|
||||
await this.acceptTerms();
|
||||
|
||||
// ❌ BAD - Related operations should stay together
|
||||
await this.fillEmailField(email);
|
||||
|
||||
await this.fillPasswordField(password);
|
||||
```
|
||||
|
||||
### Rule 5: Comments Can Define Logical Boundaries
|
||||
|
||||
Use comments to clarify why blank lines exist between units.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
async fillCredentialsForm(email: string, secret: string): Promise<void> {
|
||||
// Setup: Initialize form
|
||||
await this.openForm();
|
||||
await this.clearExistingData();
|
||||
|
||||
// Input: Fill credentials
|
||||
await this.fillEmailField(email);
|
||||
await this.fillSecretField(secret);
|
||||
|
||||
// Confirm: Submit and verify
|
||||
await this.submitForm();
|
||||
await this.waitForSuccess();
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 6: Avoid Excessive Spacing
|
||||
|
||||
Don't add blank lines between every statement. Keep related operations compact.
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Appropriate spacing density
|
||||
async fillLoginForm(credentials: Credentials): Promise<void> {
|
||||
await this.emailInput.fill(credentials.email);
|
||||
await this.passwordInput.fill(credentials.password);
|
||||
|
||||
await this.loginButton.click();
|
||||
await this.page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
// ❌ BAD - Over-spaced and hard to follow
|
||||
async fillLoginForm(credentials: Credentials): Promise<void> {
|
||||
await this.emailInput.fill(credentials.email);
|
||||
|
||||
await this.passwordInput.fill(credentials.password);
|
||||
|
||||
await this.loginButton.click();
|
||||
|
||||
await this.page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
```
|
||||
|
||||
### Spacing Checklist
|
||||
|
||||
- [ ] Use blank lines between distinct logical phases (verify → act → confirm)
|
||||
- [ ] Keep related statements together without spacing
|
||||
- [ ] Add comments if boundaries aren't obvious
|
||||
- [ ] Maximum 2-3 blank lines per function
|
||||
- [ ] Be consistent across the codebase
|
||||
|
||||
## Recent Major Migrations (January 2025)
|
||||
|
||||
- ✅ React 18 → 19.1.1 (async components, useActionState, React Compiler)
|
||||
|
||||
@@ -16,6 +16,8 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- API key management in user profile [(#8308)](https://github.com/prowler-cloud/prowler/pull/8308)
|
||||
- Refresh access token error handling [(#8864)](https://github.com/prowler-cloud/prowler/pull/8864)
|
||||
- Support Common Cloud Controls for AWS, Azure and GCP [(#8000)](https://github.com/prowler-cloud/prowler/pull/8000)
|
||||
- New M365 credentials certificate authentication method [(#8929)](https://github.com/prowler-cloud/prowler/pull/8929)
|
||||
- Centralized credentials form factory for consistent provider credential handling [(#8941)](https://github.com/prowler-cloud/prowler/pull/8941)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -4,42 +4,44 @@ import {
|
||||
AddViaCredentialsForm,
|
||||
AddViaRoleForm,
|
||||
} from "@/components/providers/workflow/forms";
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { AddViaServiceAccountForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import {
|
||||
AddViaServiceAccountForm,
|
||||
SelectViaGCP,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { getProviderFormType } from "@/lib/provider-helpers";
|
||||
getProviderFormType,
|
||||
getSelectorComponentKey,
|
||||
PROVIDER_SELECTOR_COMPONENTS,
|
||||
} from "@/lib/provider-helpers";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface Props {
|
||||
searchParams: Promise<{ type: ProviderType; id: string; via?: string }>;
|
||||
}
|
||||
|
||||
// Form type components mapping
|
||||
const FORM_COMPONENTS = {
|
||||
credentials: AddViaCredentialsForm,
|
||||
role: AddViaRoleForm,
|
||||
"service-account": AddViaServiceAccountForm,
|
||||
} as const;
|
||||
|
||||
type FormType = keyof typeof FORM_COMPONENTS;
|
||||
|
||||
export default async function AddCredentialsPage({ searchParams }: Props) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
const { type: providerType, via } = resolvedSearchParams;
|
||||
const formType = getProviderFormType(providerType, via);
|
||||
|
||||
switch (formType) {
|
||||
case "selector":
|
||||
if (providerType === "aws") return <SelectViaAWS initialVia={via} />;
|
||||
if (providerType === "gcp") return <SelectViaGCP initialVia={via} />;
|
||||
if (providerType === "github")
|
||||
return <SelectViaGitHub initialVia={via} />;
|
||||
return null;
|
||||
// Handle selector form type
|
||||
if (formType === "selector") {
|
||||
const componentKey = getSelectorComponentKey(providerType);
|
||||
if (!componentKey) return null;
|
||||
|
||||
case "credentials":
|
||||
return <AddViaCredentialsForm searchParams={resolvedSearchParams} />;
|
||||
|
||||
case "role":
|
||||
return <AddViaRoleForm searchParams={resolvedSearchParams} />;
|
||||
|
||||
case "service-account":
|
||||
return <AddViaServiceAccountForm searchParams={resolvedSearchParams} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
const SelectorComponent = PROVIDER_SELECTOR_COMPONENTS[componentKey];
|
||||
return <SelectorComponent initialVia={via} />;
|
||||
}
|
||||
|
||||
// Handle other form types
|
||||
const FormComponent = FORM_COMPONENTS[formType as FormType];
|
||||
if (!FormComponent) return null;
|
||||
|
||||
return <FormComponent searchParams={resolvedSearchParams} />;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import {
|
||||
getSelectorComponentKey,
|
||||
PROVIDER_SELECTOR_COMPONENTS,
|
||||
} from "@/lib/provider-helpers";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
interface UpdateCredentialsInfoProps {
|
||||
@@ -14,18 +15,15 @@ export const CredentialsUpdateInfo = ({
|
||||
providerType,
|
||||
initialVia,
|
||||
}: UpdateCredentialsInfoProps) => {
|
||||
const renderSelectComponent = () => {
|
||||
if (providerType === "aws") {
|
||||
return <SelectViaAWS initialVia={initialVia} />;
|
||||
}
|
||||
if (providerType === "gcp") {
|
||||
return <SelectViaGCP initialVia={initialVia} />;
|
||||
}
|
||||
if (providerType === "github") {
|
||||
return <SelectViaGitHub initialVia={initialVia} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const componentKey = getSelectorComponentKey(providerType);
|
||||
|
||||
return <div className="flex flex-col gap-4">{renderSelectComponent()}</div>;
|
||||
if (!componentKey) return null;
|
||||
|
||||
const SelectorComponent = PROVIDER_SELECTOR_COMPONENTS[componentKey];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<SelectorComponent initialVia={initialVia} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,28 +8,12 @@ import { CustomButton } from "@/components/ui/custom";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { useCredentialsForm } from "@/hooks/use-credentials-form";
|
||||
import { getAWSCredentialsTemplateLinks } from "@/lib";
|
||||
import { getCredentialFormComponent } from "@/lib/provider-credential-forms";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { requiresBackButton } from "@/lib/provider-helpers";
|
||||
import {
|
||||
AWSCredentials,
|
||||
AWSCredentialsRole,
|
||||
AzureCredentials,
|
||||
GCPDefaultCredentials,
|
||||
GCPServiceAccountKey,
|
||||
KubernetesCredentials,
|
||||
M365Credentials,
|
||||
ProviderType,
|
||||
} from "@/types";
|
||||
import { AWSCredentialsRole, ProviderType } from "@/types";
|
||||
|
||||
import { ProviderTitleDocs } from "../provider-title-docs";
|
||||
import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type";
|
||||
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 { AzureCredentialsForm } from "./via-credentials/azure-credentials-form";
|
||||
import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { M365CredentialsForm } from "./via-credentials/m365-credentials-form";
|
||||
|
||||
type BaseCredentialsFormProps = {
|
||||
providerType: ProviderType;
|
||||
@@ -63,6 +47,12 @@ export const BaseCredentialsForm = ({
|
||||
});
|
||||
|
||||
const templateLinks = getAWSCredentialsTemplateLinks(externalId);
|
||||
const credentialFormInfo = getCredentialFormComponent(
|
||||
providerType,
|
||||
searchParamsObj.get("via"),
|
||||
);
|
||||
|
||||
if (!credentialFormInfo) return null;
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -85,54 +75,27 @@ export const BaseCredentialsForm = ({
|
||||
|
||||
<Divider />
|
||||
|
||||
{providerType === "aws" && searchParamsObj.get("via") === "role" && (
|
||||
<AWSRoleCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentialsRole>}
|
||||
setValue={form.setValue as any}
|
||||
externalId={externalId}
|
||||
templateLinks={templateLinks}
|
||||
/>
|
||||
)}
|
||||
{providerType === "aws" && searchParamsObj.get("via") !== "role" && (
|
||||
<AWSStaticCredentialsForm
|
||||
control={form.control as unknown as Control<AWSCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "azure" && (
|
||||
<AzureCredentialsForm
|
||||
control={form.control as unknown as Control<AzureCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "m365" && (
|
||||
<M365CredentialsForm
|
||||
control={form.control as unknown as Control<M365Credentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") === "service-account" && (
|
||||
<GCPServiceAccountKeyForm
|
||||
control={form.control as unknown as Control<GCPServiceAccountKey>}
|
||||
{credentialFormInfo.requiresExtendedProps &&
|
||||
credentialFormInfo.passesCredentialsType === false &&
|
||||
providerType === "aws" && (
|
||||
<credentialFormInfo.component
|
||||
control={form.control as unknown as Control<AWSCredentialsRole>}
|
||||
setValue={form.setValue as any}
|
||||
externalId={externalId}
|
||||
templateLinks={templateLinks}
|
||||
/>
|
||||
)}
|
||||
{providerType === "gcp" &&
|
||||
searchParamsObj.get("via") !== "service-account" && (
|
||||
<GCPDefaultCredentialsForm
|
||||
control={
|
||||
form.control as unknown as Control<GCPDefaultCredentials>
|
||||
}
|
||||
{!credentialFormInfo.requiresExtendedProps &&
|
||||
credentialFormInfo.passesCredentialsType === true && (
|
||||
<credentialFormInfo.component
|
||||
control={form.control as unknown as Control}
|
||||
credentialsType={searchParamsObj.get("via") || undefined}
|
||||
/>
|
||||
)}
|
||||
{providerType === "kubernetes" && (
|
||||
<KubernetesCredentialsForm
|
||||
control={form.control as unknown as Control<KubernetesCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "github" && (
|
||||
<GitHubCredentialsForm
|
||||
control={form.control}
|
||||
credentialsType={searchParamsObj.get("via") || undefined}
|
||||
/>
|
||||
)}
|
||||
{!credentialFormInfo.requiresExtendedProps &&
|
||||
credentialFormInfo.passesCredentialsType === false && (
|
||||
<credentialFormInfo.component control={form.control as any} />
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-end sm:gap-6">
|
||||
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (
|
||||
@@ -159,6 +122,15 @@ export const BaseCredentialsForm = ({
|
||||
size="lg"
|
||||
isLoading={isLoading}
|
||||
endContent={!isLoading && <ChevronRightIcon size={24} />}
|
||||
onPress={(e) => {
|
||||
const formElement = e.target as HTMLElement;
|
||||
const form = formElement.closest("form");
|
||||
if (form) {
|
||||
form.dispatchEvent(
|
||||
new Event("submit", { bubbles: true, cancelable: true }),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
|
||||
</CustomButton>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { M365CertificateCredentialsForm } from "./m365-certificate-credentials-form";
|
||||
export { M365ClientSecretCredentialsForm } from "./m365-client-secret-credentials-form";
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput, CustomTextarea } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365CertificateCredentials } from "@/types";
|
||||
|
||||
export const M365CertificateCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365CertificateCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Certificate Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials with
|
||||
certificate authentication.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomTextarea
|
||||
control={control}
|
||||
name="certificate_content"
|
||||
label="Certificate Content"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the base64 encoded certificate content"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.certificate_content}
|
||||
minRows={4}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
The certificate content must be base64 encoded from an unsigned
|
||||
certificate. For detailed instructions on how to generate and encode
|
||||
your certificate, please refer to the{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/user-guide/providers/microsoft365/authentication#generate-the-certificate"
|
||||
size="sm"
|
||||
>
|
||||
certificate generation guide
|
||||
</CustomLink>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { M365ClientSecretCredentials } from "@/types";
|
||||
|
||||
export const M365ClientSecretCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365ClientSecretCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
App Client Secret Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide your Microsoft 365 application credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "./credentials-type";
|
||||
export { SelectViaM365 } from "./select-via-m365";
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { RadioGroup } from "@heroui/radio";
|
||||
import React from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
|
||||
import { CustomRadio } from "@/components/ui/custom";
|
||||
import { FormMessage } from "@/components/ui/form";
|
||||
|
||||
type 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-default-500 text-sm">
|
||||
Select Authentication Method
|
||||
</span>
|
||||
<CustomRadio
|
||||
description="Connect using Application Client Secret"
|
||||
value="app_client_secret"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Client Secret Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
<CustomRadio
|
||||
description="Connect using Application Certificate"
|
||||
value="app_certificate"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-2">App Certificate Credentials</span>
|
||||
</div>
|
||||
</CustomRadio>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
{errorMessage && (
|
||||
<FormMessage className="text-system-error dark:text-system-error">
|
||||
{errorMessage}
|
||||
</FormMessage>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from "./azure-credentials-form";
|
||||
export * from "./github-credentials-form";
|
||||
export * from "./k8s-credentials-form";
|
||||
export * from "./m365-credentials-form";
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365Credentials } from "@/types";
|
||||
|
||||
export const M365CredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<M365Credentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via Credentials
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Please provide the information for your Microsoft 365 credentials.
|
||||
</div>
|
||||
</div>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_id"
|
||||
type="text"
|
||||
label="Client ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_id}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="client_secret"
|
||||
type="password"
|
||||
label="Client Secret"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Client Secret"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.client_secret}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="tenant_id"
|
||||
type="text"
|
||||
label="Tenant ID"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Tenant ID"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
isInvalid={!!control._formState.errors.tenant_id}
|
||||
/>
|
||||
<p className="text-default-500 text-sm">
|
||||
{" "}
|
||||
User and password authentication is being deprecated due to
|
||||
Microsoft's on-going MFA enforcement across all tenants (see{" "}
|
||||
<CustomLink
|
||||
href="https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/"
|
||||
size="sm"
|
||||
>
|
||||
Microsoft docs
|
||||
</CustomLink>
|
||||
).
|
||||
</p>
|
||||
|
||||
<div className="border-system-warning bg-system-warning-medium dark:text-default-300 flex items-center rounded-lg border p-2 text-sm">
|
||||
<InfoIcon className="mr-2 inline h-4 w-4 shrink-0" />
|
||||
<p className="text-xs font-extrabold">
|
||||
By October 2025, MFA will be mandatory.
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-default-500 text-sm">
|
||||
Due to that change, you must only{" "}
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
|
||||
size="sm"
|
||||
>
|
||||
use application authentication
|
||||
</CustomLink>{" "}
|
||||
to maintain all Prowler M365 scan capabilities.
|
||||
</p>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="user"
|
||||
type="text"
|
||||
label="User"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the User"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.user}
|
||||
/>
|
||||
<CustomInput
|
||||
control={control}
|
||||
name="password"
|
||||
type="password"
|
||||
label="Password"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter the Password"
|
||||
variant="bordered"
|
||||
isRequired={false}
|
||||
isInvalid={!!control._formState.errors.password}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -40,8 +40,8 @@ export const useCredentialsForm = ({
|
||||
if (providerType === "gcp" && via === "service-account") {
|
||||
return addCredentialsServiceAccountFormSchema(providerType);
|
||||
}
|
||||
// For GitHub, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github") {
|
||||
// For GitHub and M365, we need to pass the via parameter to determine which fields are required
|
||||
if (providerType === "github" || providerType === "m365") {
|
||||
return addCredentialsFormSchema(providerType, via);
|
||||
}
|
||||
return addCredentialsFormSchema(providerType);
|
||||
@@ -99,13 +99,27 @@ export const useCredentialsForm = ({
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
case "m365":
|
||||
// M365 credentials based on via parameter
|
||||
if (via === "app_client_secret") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
if (via === "app_certificate") {
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.CLIENT_ID]: "",
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: "",
|
||||
[ProviderCredentialFields.TENANT_ID]: "",
|
||||
[ProviderCredentialFields.USER]: "",
|
||||
[ProviderCredentialFields.PASSWORD]: "",
|
||||
};
|
||||
case "gcp":
|
||||
return {
|
||||
@@ -146,9 +160,14 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const defaultValues = getDefaultValues();
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: getDefaultValues(),
|
||||
defaultValues: defaultValues,
|
||||
mode: "onSubmit",
|
||||
reValidateMode: "onChange",
|
||||
criteriaMode: "all", // Show all errors for each field
|
||||
});
|
||||
|
||||
const { handleServerResponse } = useFormServerErrors(
|
||||
@@ -169,6 +188,7 @@ export const useCredentialsForm = ({
|
||||
|
||||
// Filter out empty values first, then append all remaining values
|
||||
const filteredValues = filterEmptyValues(values);
|
||||
|
||||
Object.entries(filteredValues).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
@@ -181,9 +201,12 @@ export const useCredentialsForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { isSubmitting, errors } = form.formState;
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading: form.formState.isSubmitting,
|
||||
isLoading: isSubmitting,
|
||||
errors,
|
||||
handleSubmit,
|
||||
handleBackStep,
|
||||
searchParamsObj,
|
||||
|
||||
154
ui/lib/provider-credential-forms.ts
Normal file
154
ui/lib/provider-credential-forms.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { AWSStaticCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type";
|
||||
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
|
||||
import { GCPDefaultCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type";
|
||||
import { GCPServiceAccountKeyForm } from "@/components/providers/workflow/forms/select-credentials-type/gcp/credentials-type/gcp-service-account-key-form";
|
||||
import {
|
||||
M365CertificateCredentialsForm,
|
||||
M365ClientSecretCredentialsForm,
|
||||
} from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import { AzureCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/azure-credentials-form";
|
||||
import { GitHubCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/github-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "@/components/providers/workflow/forms/via-credentials/k8s-credentials-form";
|
||||
import { ProviderType } from "@/types/providers";
|
||||
|
||||
// Type definitions for different credential form configurations
|
||||
export type CredentialFormConfigExtended = {
|
||||
component: typeof AWSRoleCredentialsForm;
|
||||
requiresExtendedProps: true;
|
||||
passesCredentialsType: false;
|
||||
};
|
||||
|
||||
export type CredentialFormConfigPassesCredentialsType = {
|
||||
component: typeof GitHubCredentialsForm;
|
||||
requiresExtendedProps: false;
|
||||
passesCredentialsType: true;
|
||||
};
|
||||
|
||||
export type CredentialFormConfigBasic = {
|
||||
component:
|
||||
| typeof AWSStaticCredentialsForm
|
||||
| typeof GCPServiceAccountKeyForm
|
||||
| typeof GCPDefaultCredentialsForm
|
||||
| typeof M365ClientSecretCredentialsForm
|
||||
| typeof M365CertificateCredentialsForm
|
||||
| typeof AzureCredentialsForm
|
||||
| typeof KubernetesCredentialsForm;
|
||||
requiresExtendedProps: false;
|
||||
passesCredentialsType: false;
|
||||
};
|
||||
|
||||
export type CredentialFormConfig =
|
||||
| CredentialFormConfigExtended
|
||||
| CredentialFormConfigPassesCredentialsType
|
||||
| CredentialFormConfigBasic;
|
||||
|
||||
// Provider credential form components mapping
|
||||
const PROVIDER_CREDENTIAL_FORMS = {
|
||||
aws: {
|
||||
role: AWSRoleCredentialsForm,
|
||||
credentials: AWSStaticCredentialsForm,
|
||||
},
|
||||
gcp: {
|
||||
"service-account": GCPServiceAccountKeyForm,
|
||||
credentials: GCPDefaultCredentialsForm,
|
||||
},
|
||||
m365: {
|
||||
app_client_secret: M365ClientSecretCredentialsForm,
|
||||
app_certificate: M365CertificateCredentialsForm,
|
||||
},
|
||||
azure: {
|
||||
default: AzureCredentialsForm,
|
||||
},
|
||||
kubernetes: {
|
||||
default: KubernetesCredentialsForm,
|
||||
},
|
||||
github: {
|
||||
default: GitHubCredentialsForm,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Strategy dictionary: maps (provider + via) to credential form config
|
||||
type ProviderViaKey =
|
||||
| "aws:role"
|
||||
| "aws"
|
||||
| "gcp:service-account"
|
||||
| "gcp"
|
||||
| "m365:app_client_secret"
|
||||
| "m365:app_certificate"
|
||||
| "github"
|
||||
| "azure"
|
||||
| "kubernetes";
|
||||
|
||||
const PROVIDER_VIA_STRATEGIES = {
|
||||
// AWS strategies
|
||||
"aws:role": () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.aws.role,
|
||||
requiresExtendedProps: true,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
aws: () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.aws.credentials,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
|
||||
// GCP strategies
|
||||
"gcp:service-account": () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.gcp["service-account"],
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
gcp: () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.gcp.credentials,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
|
||||
// M365 strategies
|
||||
"m365:app_client_secret": () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.m365.app_client_secret,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
"m365:app_certificate": () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.m365.app_certificate,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
|
||||
// GitHub strategy
|
||||
github: () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.github.default,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: true,
|
||||
}),
|
||||
|
||||
// Azure strategy
|
||||
azure: () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.azure.default,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
|
||||
// Kubernetes strategy
|
||||
kubernetes: () => ({
|
||||
component: PROVIDER_CREDENTIAL_FORMS.kubernetes.default,
|
||||
requiresExtendedProps: false,
|
||||
passesCredentialsType: false,
|
||||
}),
|
||||
} satisfies Record<ProviderViaKey, () => CredentialFormConfig>;
|
||||
|
||||
// Helper to get credential form component based on provider and via parameter
|
||||
export const getCredentialFormComponent = (
|
||||
provider: ProviderType,
|
||||
via?: string | null,
|
||||
): CredentialFormConfig | null => {
|
||||
// Build strategy key: use "provider:via" if via exists, otherwise just "provider"
|
||||
const strategyKey = via ? `${provider}:${via}` : provider;
|
||||
|
||||
const strategy = PROVIDER_VIA_STRATEGIES[strategyKey as ProviderViaKey];
|
||||
|
||||
return strategy ? strategy() : null;
|
||||
};
|
||||
@@ -80,14 +80,21 @@ export const buildAzureSecret = (formData: FormData) => {
|
||||
|
||||
export const buildM365Secret = (formData: FormData) => {
|
||||
const secret = {
|
||||
...buildAzureSecret(formData),
|
||||
[ProviderCredentialFields.USER]: getFormValue(
|
||||
[ProviderCredentialFields.CLIENT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.USER,
|
||||
ProviderCredentialFields.CLIENT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.PASSWORD]: getFormValue(
|
||||
[ProviderCredentialFields.TENANT_ID]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.PASSWORD,
|
||||
ProviderCredentialFields.TENANT_ID,
|
||||
),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CLIENT_SECRET,
|
||||
),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.CERTIFICATE_CONTENT,
|
||||
),
|
||||
};
|
||||
return filterEmptyValues(secret);
|
||||
|
||||
@@ -29,6 +29,7 @@ export const ProviderCredentialFields = {
|
||||
TENANT_ID: "tenant_id",
|
||||
USER: "user",
|
||||
PASSWORD: "password",
|
||||
CERTIFICATE_CONTENT: "certificate_content",
|
||||
|
||||
// GCP fields
|
||||
REFRESH_TOKEN: "refresh_token",
|
||||
@@ -70,6 +71,7 @@ export const ErrorPointers = {
|
||||
OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token",
|
||||
GITHUB_APP_ID: "/data/attributes/secret/github_app_id",
|
||||
GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content",
|
||||
CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
|
||||
import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp";
|
||||
import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github";
|
||||
import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365";
|
||||
import {
|
||||
ProviderEntity,
|
||||
ProviderProps,
|
||||
@@ -53,7 +57,7 @@ export const getProviderFormType = (
|
||||
via?: string,
|
||||
): ProviderFormType => {
|
||||
// Providers that need credential type selection
|
||||
const needsSelector = ["aws", "gcp", "github"].includes(providerType);
|
||||
const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType);
|
||||
|
||||
// Show selector if no via parameter and provider needs it
|
||||
if (needsSelector && !via) {
|
||||
@@ -80,6 +84,14 @@ export const getProviderFormType = (
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// M365 credential types
|
||||
if (
|
||||
providerType === "m365" &&
|
||||
["app_client_secret", "app_certificate"].includes(via || "")
|
||||
) {
|
||||
return "credentials";
|
||||
}
|
||||
|
||||
// Other providers go directly to credentials form
|
||||
if (!needsSelector) {
|
||||
return "credentials";
|
||||
@@ -99,7 +111,34 @@ export const requiresBackButton = (via?: string | null): boolean => {
|
||||
"personal_access_token",
|
||||
"oauth_app",
|
||||
"github_app",
|
||||
"app_client_secret",
|
||||
"app_certificate",
|
||||
];
|
||||
|
||||
return validViaTypes.includes(via);
|
||||
};
|
||||
|
||||
// Provider selector components mapping
|
||||
export const PROVIDER_SELECTOR_COMPONENTS = {
|
||||
AWS: SelectViaAWS,
|
||||
GCP: SelectViaGCP,
|
||||
GITHUB: SelectViaGitHub,
|
||||
M365: SelectViaM365,
|
||||
} as const;
|
||||
|
||||
export type SelectorProvider = keyof typeof PROVIDER_SELECTOR_COMPONENTS;
|
||||
|
||||
// Helper to map ProviderType to SelectorProvider key
|
||||
export const getSelectorComponentKey = (
|
||||
provider: ProviderType,
|
||||
): SelectorProvider | null => {
|
||||
const keyMap: Record<ProviderType, SelectorProvider | null> = {
|
||||
aws: "AWS",
|
||||
azure: null,
|
||||
gcp: "GCP",
|
||||
github: "GITHUB",
|
||||
kubernetes: null,
|
||||
m365: "M365",
|
||||
};
|
||||
return keyMap[provider] ?? null;
|
||||
};
|
||||
|
||||
@@ -213,15 +213,24 @@ export type AzureCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials = {
|
||||
export type M365ClientSecretCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.USER]?: string;
|
||||
[ProviderCredentialFields.PASSWORD]?: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365CertificateCredentials = {
|
||||
[ProviderCredentialFields.CLIENT_ID]: string;
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: string;
|
||||
[ProviderCredentialFields.TENANT_ID]: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type M365Credentials =
|
||||
| M365ClientSecretCredentials
|
||||
| M365CertificateCredentials;
|
||||
|
||||
export type GCPDefaultCredentials = {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
|
||||
@@ -168,12 +168,13 @@ export const addCredentialsFormSchema = (
|
||||
.min(1, "Client ID is required"),
|
||||
[ProviderCredentialFields.CLIENT_SECRET]: z
|
||||
.string()
|
||||
.min(1, "Client Secret is required"),
|
||||
.optional(),
|
||||
[ProviderCredentialFields.CERTIFICATE_CONTENT]: z
|
||||
.string()
|
||||
.optional(),
|
||||
[ProviderCredentialFields.TENANT_ID]: z
|
||||
.string()
|
||||
.min(1, "Tenant ID is required"),
|
||||
[ProviderCredentialFields.USER]: z.string().optional(),
|
||||
[ProviderCredentialFields.PASSWORD]: z.string().optional(),
|
||||
}
|
||||
: providerType === "github"
|
||||
? {
|
||||
@@ -194,23 +195,26 @@ export const addCredentialsFormSchema = (
|
||||
})
|
||||
.superRefine((data: Record<string, any>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
const hasUser = !!data[ProviderCredentialFields.USER];
|
||||
const hasPassword = !!data[ProviderCredentialFields.PASSWORD];
|
||||
|
||||
if (hasUser && !hasPassword) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a user, you must also provide a password",
|
||||
path: [ProviderCredentialFields.PASSWORD],
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPassword && !hasUser) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "If you provide a password, you must also provide a user",
|
||||
path: [ProviderCredentialFields.USER],
|
||||
});
|
||||
// Validate based on the via parameter
|
||||
if (via === "app_client_secret") {
|
||||
const clientSecret = data[ProviderCredentialFields.CLIENT_SECRET];
|
||||
if (!clientSecret || clientSecret.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Client Secret is required",
|
||||
path: [ProviderCredentialFields.CLIENT_SECRET],
|
||||
});
|
||||
}
|
||||
} else if (via === "app_certificate") {
|
||||
const certificateContent =
|
||||
data[ProviderCredentialFields.CERTIFICATE_CONTENT];
|
||||
if (!certificateContent || certificateContent.trim() === "") {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
message: "Certificate Content is required",
|
||||
path: [ProviderCredentialFields.CERTIFICATE_CONTENT],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +223,7 @@ export const addCredentialsFormSchema = (
|
||||
if (via === "personal_access_token") {
|
||||
if (!data[ProviderCredentialFields.PERSONAL_ACCESS_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "Personal Access Token is required",
|
||||
path: [ProviderCredentialFields.PERSONAL_ACCESS_TOKEN],
|
||||
});
|
||||
@@ -227,7 +231,7 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "oauth_app") {
|
||||
if (!data[ProviderCredentialFields.OAUTH_APP_TOKEN]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "OAuth App Token is required",
|
||||
path: [ProviderCredentialFields.OAUTH_APP_TOKEN],
|
||||
});
|
||||
@@ -235,14 +239,14 @@ export const addCredentialsFormSchema = (
|
||||
} else if (via === "github_app") {
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_ID]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App ID is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_ID],
|
||||
});
|
||||
}
|
||||
if (!data[ProviderCredentialFields.GITHUB_APP_KEY]) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: "GitHub App Private Key is required",
|
||||
path: [ProviderCredentialFields.GITHUB_APP_KEY],
|
||||
});
|
||||
@@ -390,7 +394,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const yamlValidation = validateYaml(val);
|
||||
if (!yamlValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid YAML format: ${yamlValidation.error}`,
|
||||
});
|
||||
return;
|
||||
@@ -399,7 +403,7 @@ export const mutedFindingsConfigFormSchema = z.object({
|
||||
const mutelistValidation = validateMutelistYaml(val);
|
||||
if (!mutelistValidation.isValid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
code: "custom",
|
||||
message: `Invalid mutelist structure: ${mutelistValidation.error}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,55 +9,70 @@ export const PROVIDER_TYPES = [
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
|
||||
interface ProviderConnection {
|
||||
connected: boolean;
|
||||
last_checked_at: string;
|
||||
}
|
||||
|
||||
interface ScannerArgs {
|
||||
only_logs: boolean;
|
||||
excluded_checks: string[];
|
||||
aws_retries_max_attempts: number;
|
||||
}
|
||||
|
||||
interface CreatedBy {
|
||||
object: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ProviderAttributes {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
status: "completed" | "pending" | "cancelled";
|
||||
resources: number;
|
||||
connection: ProviderConnection;
|
||||
scanner_args: ScannerArgs;
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
created_by: CreatedBy;
|
||||
}
|
||||
|
||||
interface ApiEntity {
|
||||
type: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface RelationshipData {
|
||||
data: ApiEntity | null;
|
||||
}
|
||||
|
||||
interface ProviderGroupMeta {
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ProviderGroupRelationship {
|
||||
meta: ProviderGroupMeta;
|
||||
data: ApiEntity[];
|
||||
}
|
||||
|
||||
interface ProviderRelationships {
|
||||
secret: RelationshipData;
|
||||
provider_groups: ProviderGroupRelationship;
|
||||
}
|
||||
|
||||
export interface ProviderProps {
|
||||
id: string;
|
||||
type: "providers";
|
||||
attributes: {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string;
|
||||
status: "completed" | "pending" | "cancelled";
|
||||
resources: number;
|
||||
connection: {
|
||||
connected: boolean;
|
||||
last_checked_at: string;
|
||||
};
|
||||
scanner_args: {
|
||||
only_logs: boolean;
|
||||
excluded_checks: string[];
|
||||
aws_retries_max_attempts: number;
|
||||
};
|
||||
inserted_at: string;
|
||||
updated_at: string;
|
||||
created_by: {
|
||||
object: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
relationships: {
|
||||
secret: {
|
||||
data: {
|
||||
type: string;
|
||||
id: string;
|
||||
} | null;
|
||||
};
|
||||
provider_groups: {
|
||||
meta: {
|
||||
count: number;
|
||||
};
|
||||
data: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
attributes: ProviderAttributes;
|
||||
relationships: ProviderRelationships;
|
||||
groupNames?: string[];
|
||||
}
|
||||
|
||||
export interface ProviderEntity {
|
||||
provider: ProviderType;
|
||||
uid: string;
|
||||
alias: string | null;
|
||||
provider: ProviderType;
|
||||
}
|
||||
|
||||
export interface ProviderConnectionStatus {
|
||||
@@ -65,47 +80,65 @@ export interface ProviderConnectionStatus {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface ProviderFinding {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ProviderResources {
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface ProviderOverviewAttributes {
|
||||
findings: ProviderFinding;
|
||||
resources: ProviderResources;
|
||||
}
|
||||
|
||||
interface ProviderOverviewData {
|
||||
type: "provider-overviews";
|
||||
id: ProviderType;
|
||||
attributes: ProviderOverviewAttributes;
|
||||
}
|
||||
|
||||
interface ResponseMeta {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ProviderOverviewProps {
|
||||
data: {
|
||||
type: "provider-overviews";
|
||||
id: ProviderType;
|
||||
attributes: {
|
||||
findings: {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
total: number;
|
||||
};
|
||||
resources: {
|
||||
total: number;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
data: ProviderOverviewData[];
|
||||
meta: ResponseMeta;
|
||||
}
|
||||
|
||||
interface PaginationMeta {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface ApiResponseMeta {
|
||||
pagination: PaginationMeta;
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface ApiLinks {
|
||||
first: string;
|
||||
last: string;
|
||||
next: string | null;
|
||||
prev: string | null;
|
||||
}
|
||||
|
||||
interface IncludedEntity {
|
||||
type: string;
|
||||
id: string;
|
||||
attributes?: any;
|
||||
relationships?: any;
|
||||
}
|
||||
|
||||
export interface ProvidersApiResponse {
|
||||
links: {
|
||||
first: string;
|
||||
last: string;
|
||||
next: string | null;
|
||||
prev: string | null;
|
||||
};
|
||||
links: ApiLinks;
|
||||
data: ProviderProps[];
|
||||
included?: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
}>;
|
||||
meta: {
|
||||
pagination: {
|
||||
page: number;
|
||||
pages: number;
|
||||
count: number;
|
||||
};
|
||||
version: string;
|
||||
};
|
||||
included?: IncludedEntity[];
|
||||
meta: ApiResponseMeta;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user