feat(ui): add OpenStack provider support (#10046)

Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
Daniel Barranquero
2026-02-20 09:44:34 +01:00
committed by GitHub
parent ea60f2d082
commit 40f6a7133d
21 changed files with 187 additions and 2 deletions

View File

@@ -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)

View File

@@ -13,6 +13,7 @@ import {
KS8ProviderBadge,
M365ProviderBadge,
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
} from "@/components/icons/providers-badge";
import {
@@ -36,6 +37,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
oraclecloud: <OracleCloudProviderBadge width={18} height={18} />,
mongodbatlas: <MongoDBAtlasProviderBadge width={18} height={18} />,
alibabacloud: <AlibabaCloudProviderBadge width={18} height={18} />,
openstack: <OpenStackProviderBadge width={18} height={18} />,
};
interface AccountsSelectorProps {

View File

@@ -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 = {

View File

@@ -8,6 +8,7 @@ import {
KS8ProviderBadge,
M365ProviderBadge,
MongoDBAtlasProviderBadge,
OpenStackProviderBadge,
OracleCloudProviderBadge,
} from "../icons/providers-badge";
@@ -100,3 +101,12 @@ export const CustomProviderInputAlibabaCloud = () => {
</div>
);
};
export const CustomProviderInputOpenStack = () => {
return (
<div className="flex items-center gap-x-2">
<OpenStackProviderBadge width={25} height={25} />
<p className="text-sm">OpenStack</p>
</div>
);
};

View File

@@ -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 {

View File

@@ -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<string, FC<IconSvgProps>> = {
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,
"MongoDB Atlas": MongoDBAtlasProviderBadge,
"Alibaba Cloud": AlibabaCloudProviderBadge,
OpenStack: OpenStackProviderBadge,
};

View File

@@ -0,0 +1,29 @@
import * as React from "react";
import { IconSvgProps } from "@/types";
export const OpenStackProviderBadge: React.FC<IconSvgProps> = ({
size,
width,
height,
...props
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
fill="none"
focusable="false"
height={size || height}
role="presentation"
viewBox="0 0 256 256"
width={size || width}
{...props}
>
<g fill="none">
<rect width="256" height="256" fill="#f4f2ed" rx="60" />
<g transform="translate(48 48) scale(2.5)" fill="#da1a32">
<path d="M58.054.68H5.946C2.676.68 0 3.356 0 6.626V20.64h14.452v-2.3c0-1.776 1.44-3.215 3.215-3.215h28.665c1.776 0 3.215 1.44 3.215 3.215v2.3H64v-14A5.97 5.97 0 0 0 58.054.68zm-8.506 44.97c0 1.776-1.44 3.215-3.215 3.215H17.67c-1.776 0-3.215-1.44-3.215-3.215v-2.3H0v14.013c0 3.27 2.676 5.946 5.946 5.946h52.108c3.27 0 5.946-2.676 5.946-5.946V43.36H49.548zM0 24.773h14.452v14.452H0zm49.548 0H64v14.452H49.548z" />
</g>
</g>
</svg>
);

View File

@@ -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 {

View File

@@ -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" && (
<OpenStackCredentialsForm
control={form.control as unknown as Control<OpenStackCredentials>}
/>
)}
<div className="flex w-full justify-end gap-4">
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (

View File

@@ -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",

View File

@@ -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";

View File

@@ -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<OpenStackCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
Connect via Clouds YAML
</div>
<div className="text-default-500 text-sm">
Please provide your OpenStack clouds.yaml content and the cloud name.
</div>
</div>
<CustomTextarea
control={control}
name="clouds_yaml_content"
label="Clouds YAML Content"
labelPlacement="inside"
placeholder="Paste your clouds.yaml content here"
variant="bordered"
minRows={10}
isRequired
/>
<CustomInput
control={control}
name="clouds_yaml_cloud"
type="text"
label="Cloud Name"
labelPlacement="inside"
placeholder="e.g. mycloud"
variant="bordered"
isRequired
/>
</>
);
};

View File

@@ -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 <MongoDBAtlasProviderBadge width={35} height={35} />;
case "alibabacloud":
return <AlibabaCloudProviderBadge width={35} height={35} />;
case "openstack":
return <OpenStackProviderBadge width={35} height={35} />;
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";
}

View File

@@ -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;
}

View File

@@ -30,4 +30,8 @@ export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record<string, string> = {
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,
};

View File

@@ -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?",

View File

@@ -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];

View File

@@ -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];

View File

@@ -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;

View File

@@ -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<string, string | undefined>, ctx) => {
if (providerType === "m365") {

View File

@@ -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<ProviderType, string> = {
iac: "Infrastructure as Code",
oraclecloud: "Oracle Cloud Infrastructure",
alibabacloud: "Alibaba Cloud",
openstack: "OpenStack",
};
export function getProviderDisplayName(providerId: string): string {