From 40f6a7133da5e0b9d75e110bd4c2e7e256dce14b Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Fri, 20 Feb 2026 09:44:34 +0100 Subject: [PATCH] feat(ui): add OpenStack provider support (#10046) Co-authored-by: alejandrobailo --- ui/CHANGELOG.md | 1 + .../_components/accounts-selector.tsx | 2 + .../_components/provider-type-selector.tsx | 9 ++++ .../filters/custom-provider-inputs.tsx | 10 +++++ .../findings/table/provider-icon-cell.tsx | 2 + ui/components/icons/providers-badge/index.ts | 3 ++ .../openstack-provider-badge.tsx | 29 +++++++++++++ .../providers/radio-group-provider.tsx | 6 +++ .../workflow/forms/base-credentials-form.tsx | 7 +++ .../workflow/forms/connect-account-form.tsx | 5 +++ .../workflow/forms/via-credentials/index.ts | 1 + .../openstack-credentials-form.tsx | 43 +++++++++++++++++++ .../ui/entities/get-provider-logo.tsx | 5 +++ ui/hooks/use-credentials-form.ts | 6 +++ ui/lib/error-mappings.ts | 4 ++ ui/lib/external-urls.ts | 5 +++ .../build-crendentials.ts | 18 ++++++++ .../provider-credential-fields.ts | 6 +++ ui/types/components.ts | 9 +++- ui/types/formSchemas.ts | 16 ++++++- ui/types/providers.ts | 2 + 21 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 ui/components/icons/providers-badge/openstack-provider-badge.tsx create mode 100644 ui/components/providers/workflow/forms/via-credentials/openstack-credentials-form.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index ae3d1bfbba..db79ce2a93 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added +- OpenStack provider support in the UI [(#10046)](https://github.com/prowler-cloud/prowler/pull/10046) - PDF report available for the CSA CCM compliance framework [(#10088)](https://github.com/prowler-cloud/prowler/pull/10088) - CSV and PDF download buttons in compliance views [(#10093)](https://github.com/prowler-cloud/prowler/pull/10093) diff --git a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx index 98bb9df9f5..8536ce3fa4 100644 --- a/ui/app/(prowler)/_overview/_components/accounts-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/accounts-selector.tsx @@ -13,6 +13,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, } from "@/components/icons/providers-badge"; import { @@ -36,6 +37,7 @@ const PROVIDER_ICON: Record = { oraclecloud: , mongodbatlas: , alibabacloud: , + openstack: , }; interface AccountsSelectorProps { diff --git a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx index d9457a499b..a649bc3843 100644 --- a/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx +++ b/ui/app/(prowler)/_overview/_components/provider-type-selector.tsx @@ -63,6 +63,11 @@ const AlibabaCloudProviderBadge = lazy(() => default: m.AlibabaCloudProviderBadge, })), ); +const OpenStackProviderBadge = lazy(() => + import("@/components/icons/providers-badge").then((m) => ({ + default: m.OpenStackProviderBadge, + })), +); type IconProps = { width: number; height: number }; @@ -114,6 +119,10 @@ const PROVIDER_DATA: Record< label: "Alibaba Cloud", icon: AlibabaCloudProviderBadge, }, + openstack: { + label: "OpenStack", + icon: OpenStackProviderBadge, + }, }; type ProviderTypeSelectorProps = { diff --git a/ui/components/filters/custom-provider-inputs.tsx b/ui/components/filters/custom-provider-inputs.tsx index 47fe62e3aa..3bb5b1eb66 100644 --- a/ui/components/filters/custom-provider-inputs.tsx +++ b/ui/components/filters/custom-provider-inputs.tsx @@ -8,6 +8,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, } from "../icons/providers-badge"; @@ -100,3 +101,12 @@ export const CustomProviderInputAlibabaCloud = () => { ); }; + +export const CustomProviderInputOpenStack = () => { + return ( +
+ +

OpenStack

+
+ ); +}; diff --git a/ui/components/findings/table/provider-icon-cell.tsx b/ui/components/findings/table/provider-icon-cell.tsx index a04341b4d2..31c4c64475 100644 --- a/ui/components/findings/table/provider-icon-cell.tsx +++ b/ui/components/findings/table/provider-icon-cell.tsx @@ -8,6 +8,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, } from "@/components/icons/providers-badge"; import { ProviderType } from "@/types"; @@ -23,6 +24,7 @@ export const PROVIDER_ICONS = { oraclecloud: OracleCloudProviderBadge, mongodbatlas: MongoDBAtlasProviderBadge, alibabacloud: AlibabaCloudProviderBadge, + openstack: OpenStackProviderBadge, } as const; interface ProviderIconCellProps { diff --git a/ui/components/icons/providers-badge/index.ts b/ui/components/icons/providers-badge/index.ts index 58f5a0b260..dbc86a840a 100644 --- a/ui/components/icons/providers-badge/index.ts +++ b/ui/components/icons/providers-badge/index.ts @@ -11,6 +11,7 @@ import { IacProviderBadge } from "./iac-provider-badge"; import { KS8ProviderBadge } from "./ks8-provider-badge"; import { M365ProviderBadge } from "./m365-provider-badge"; import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge"; +import { OpenStackProviderBadge } from "./openstack-provider-badge"; import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge"; export { @@ -23,6 +24,7 @@ export { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, }; @@ -38,4 +40,5 @@ export const PROVIDER_ICONS: Record> = { "Oracle Cloud Infrastructure": OracleCloudProviderBadge, "MongoDB Atlas": MongoDBAtlasProviderBadge, "Alibaba Cloud": AlibabaCloudProviderBadge, + OpenStack: OpenStackProviderBadge, }; diff --git a/ui/components/icons/providers-badge/openstack-provider-badge.tsx b/ui/components/icons/providers-badge/openstack-provider-badge.tsx new file mode 100644 index 0000000000..5ccf427236 --- /dev/null +++ b/ui/components/icons/providers-badge/openstack-provider-badge.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; + +import { IconSvgProps } from "@/types"; + +export const OpenStackProviderBadge: React.FC = ({ + size, + width, + height, + ...props +}) => ( + +); diff --git a/ui/components/providers/radio-group-provider.tsx b/ui/components/providers/radio-group-provider.tsx index de4f8596b6..475df042db 100644 --- a/ui/components/providers/radio-group-provider.tsx +++ b/ui/components/providers/radio-group-provider.tsx @@ -18,6 +18,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, } from "../icons/providers-badge"; import { FormMessage } from "../ui/form"; @@ -73,6 +74,11 @@ const PROVIDERS = [ label: "Alibaba Cloud", badge: AlibabaCloudProviderBadge, }, + { + value: "openstack", + label: "OpenStack", + badge: OpenStackProviderBadge, + }, ] as const; interface RadioGroupProviderProps { diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index d200a1e1c8..ddfeb1ead2 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -25,6 +25,7 @@ import { M365ClientSecretCredentials, MongoDBAtlasCredentials, OCICredentials, + OpenStackCredentials, ProviderType, } from "@/types"; @@ -46,6 +47,7 @@ import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form import { IacCredentialsForm } from "./via-credentials/iac-credentials-form"; import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form"; +import { OpenStackCredentialsForm } from "./via-credentials/openstack-credentials-form"; import { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form"; type BaseCredentialsFormProps = { @@ -206,6 +208,11 @@ export const BaseCredentialsForm = ({ } /> )} + {providerType === "openstack" && ( + } + /> + )}
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && ( diff --git a/ui/components/providers/workflow/forms/connect-account-form.tsx b/ui/components/providers/workflow/forms/connect-account-form.tsx index b6b50ead52..15c4d7f8d1 100644 --- a/ui/components/providers/workflow/forms/connect-account-form.tsx +++ b/ui/components/providers/workflow/forms/connect-account-form.tsx @@ -72,6 +72,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => { label: "Account ID", placeholder: "e.g. 1234567890123456", }; + case "openstack": + return { + label: "Project ID", + placeholder: "e.g. a1b2c3d4-e5f6-7890-abcd-ef1234567890", + }; default: return { label: "Provider UID", diff --git a/ui/components/providers/workflow/forms/via-credentials/index.ts b/ui/components/providers/workflow/forms/via-credentials/index.ts index 6c4c7dfeae..2b44d85485 100644 --- a/ui/components/providers/workflow/forms/via-credentials/index.ts +++ b/ui/components/providers/workflow/forms/via-credentials/index.ts @@ -3,3 +3,4 @@ export * from "./github-credentials-form"; export * from "./iac-credentials-form"; export * from "./k8s-credentials-form"; export * from "./mongodbatlas-credentials-form"; +export * from "./openstack-credentials-form"; diff --git a/ui/components/providers/workflow/forms/via-credentials/openstack-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials/openstack-credentials-form.tsx new file mode 100644 index 0000000000..24ce2fbdb9 --- /dev/null +++ b/ui/components/providers/workflow/forms/via-credentials/openstack-credentials-form.tsx @@ -0,0 +1,43 @@ +import { Control } from "react-hook-form"; + +import { CustomInput, CustomTextarea } from "@/components/ui/custom"; +import { OpenStackCredentials } from "@/types"; + +export const OpenStackCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ Connect via Clouds YAML +
+
+ Please provide your OpenStack clouds.yaml content and the cloud name. +
+
+ + + + ); +}; diff --git a/ui/components/ui/entities/get-provider-logo.tsx b/ui/components/ui/entities/get-provider-logo.tsx index 54520b9f86..7a0e649455 100644 --- a/ui/components/ui/entities/get-provider-logo.tsx +++ b/ui/components/ui/entities/get-provider-logo.tsx @@ -8,6 +8,7 @@ import { KS8ProviderBadge, M365ProviderBadge, MongoDBAtlasProviderBadge, + OpenStackProviderBadge, OracleCloudProviderBadge, } from "@/components/icons/providers-badge"; import { ProviderType } from "@/types"; @@ -34,6 +35,8 @@ export const getProviderLogo = (provider: ProviderType) => { return ; case "alibabacloud": return ; + case "openstack": + return ; default: return null; } @@ -61,6 +64,8 @@ export const getProviderName = (provider: ProviderType): string => { return "MongoDB Atlas"; case "alibabacloud": return "Alibaba Cloud"; + case "openstack": + return "OpenStack"; default: return "Unknown Provider"; } diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts index 0e388fd9e9..d2e5d81352 100644 --- a/ui/hooks/use-credentials-form.ts +++ b/ui/hooks/use-credentials-form.ts @@ -192,6 +192,12 @@ export const useCredentialsForm = ({ [ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: "", [ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: "", }; + case "openstack": + return { + ...baseDefaults, + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: "", + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: "", + }; default: return baseDefaults; } diff --git a/ui/lib/error-mappings.ts b/ui/lib/error-mappings.ts index 2c5eafcb2b..ae9afb8414 100644 --- a/ui/lib/error-mappings.ts +++ b/ui/lib/error-mappings.ts @@ -30,4 +30,8 @@ export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record = { ProviderCredentialFields.SERVICE_ACCOUNT_KEY, [ErrorPointers.ATLAS_PUBLIC_KEY]: ProviderCredentialFields.ATLAS_PUBLIC_KEY, [ErrorPointers.ATLAS_PRIVATE_KEY]: ProviderCredentialFields.ATLAS_PRIVATE_KEY, + [ErrorPointers.OPENSTACK_CLOUDS_YAML_CONTENT]: + ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT, + [ErrorPointers.OPENSTACK_CLOUDS_YAML_CLOUD]: + ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD, }; diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index db3ab59231..8c55eb35c5 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -58,6 +58,11 @@ export const getProviderHelpText = (provider: string) => { text: "Need help connecting your Alibaba Cloud account?", link: "https://goto.prowler.com/provider-alibabacloud", }; + case "openstack": + return { + text: "Need help connecting your OpenStack cloud?", + link: "https://goto.prowler.com/provider-openstack", + }; default: return { text: "How to setup a provider?", diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts index b48492c804..4d25fd68f9 100644 --- a/ui/lib/provider-credentials/build-crendentials.ts +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -250,6 +250,20 @@ export const buildAlibabaCloudSecret = ( return filterEmptyValues(secret); }; +export const buildOpenStackSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue( + formData, + ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT, + ), + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: getFormValue( + formData, + ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD, + ), + }; + return filterEmptyValues(secret); +}; + export const buildIacSecret = (formData: FormData) => { const secret = { [ProviderCredentialFields.REPOSITORY_URL]: getFormValue( @@ -373,6 +387,10 @@ export const buildSecretConfig = ( secret: buildAlibabaCloudSecret(formData, isRole), }; }, + openstack: () => ({ + secretType: "static", + secret: buildOpenStackSecret(formData), + }), }; const builder = secretBuilders[providerType]; diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts index 8feb7434c7..637565e312 100644 --- a/ui/lib/provider-credentials/provider-credential-fields.ts +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -67,6 +67,10 @@ export const ProviderCredentialFields = { ALIBABACLOUD_ACCESS_KEY_SECRET: "access_key_secret", ALIBABACLOUD_ROLE_ARN: "role_arn", ALIBABACLOUD_ROLE_SESSION_NAME: "role_session_name", + + // OpenStack fields + OPENSTACK_CLOUDS_YAML_CONTENT: "clouds_yaml_content", + OPENSTACK_CLOUDS_YAML_CLOUD: "clouds_yaml_cloud", } as const; // Type for credential field values @@ -111,6 +115,8 @@ export const ErrorPointers = { ALIBABACLOUD_ACCESS_KEY_SECRET: "/data/attributes/secret/access_key_secret", ALIBABACLOUD_ROLE_ARN: "/data/attributes/secret/role_arn", ALIBABACLOUD_ROLE_SESSION_NAME: "/data/attributes/secret/role_session_name", + OPENSTACK_CLOUDS_YAML_CONTENT: "/data/attributes/secret/clouds_yaml_content", + OPENSTACK_CLOUDS_YAML_CLOUD: "/data/attributes/secret/clouds_yaml_cloud", } as const; export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/types/components.ts b/ui/types/components.ts index ad650e74da..ea2813a8b4 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -334,6 +334,12 @@ export type AlibabaCloudCredentialsRole = { [ProviderCredentialFields.PROVIDER_ID]: string; }; +export type OpenStackCredentials = { + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: string; + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; +}; + export type CredentialsFormSchema = | AWSCredentials | AWSCredentialsRole @@ -346,7 +352,8 @@ export type CredentialsFormSchema = | OCICredentials | MongoDBAtlasCredentials | AlibabaCloudCredentials - | AlibabaCloudCredentialsRole; + | AlibabaCloudCredentialsRole + | OpenStackCredentials; export interface SearchParamsProps { [key: string]: string | string[] | undefined; diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index 5207e8b926..36c4e724d0 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -130,6 +130,11 @@ export const addProviderFormSchema = z [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), }), + z.object({ + providerType: z.literal("openstack"), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), + providerUid: z.string(), + }), ]), ); @@ -259,7 +264,16 @@ export const addCredentialsFormSchema = ( .string() .min(1, "Access Key Secret is required"), } - : {}), + : providerType === "openstack" + ? { + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: + z + .string() + .min(1, "Clouds YAML content is required"), + [ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CLOUD]: + z.string().min(1, "Cloud name is required"), + } + : {}), }) .superRefine((data: Record, ctx) => { if (providerType === "m365") { diff --git a/ui/types/providers.ts b/ui/types/providers.ts index e4a5d3aae7..6b164c7b83 100644 --- a/ui/types/providers.ts +++ b/ui/types/providers.ts @@ -9,6 +9,7 @@ export const PROVIDER_TYPES = [ "iac", "oraclecloud", "alibabacloud", + "openstack", ] as const; export type ProviderType = (typeof PROVIDER_TYPES)[number]; @@ -24,6 +25,7 @@ export const PROVIDER_DISPLAY_NAMES: Record = { iac: "Infrastructure as Code", oraclecloud: "Oracle Cloud Infrastructure", alibabacloud: "Alibaba Cloud", + openstack: "OpenStack", }; export function getProviderDisplayName(providerId: string): string {