feat(ui): support AlibabaCloud provider

This commit is contained in:
pedrooot
2025-12-10 15:10:24 +01:00
parent 56c8be226d
commit eff42ff9fc
27 changed files with 644 additions and 63 deletions
@@ -2,6 +2,14 @@ import { getProviderDisplayName } from "@/types/providers";
import { RegionsOverviewResponse } from "./types";
export const RISK_LEVELS = {
LOW_HIGH: "low-high",
HIGH: "high",
CRITICAL: "critical",
} as const;
export type RiskLevel = (typeof RISK_LEVELS)[keyof typeof RISK_LEVELS];
export interface ThreatMapLocation {
id: string;
name: string;
@@ -11,7 +19,7 @@ export interface ThreatMapLocation {
coordinates: [number, number];
totalFindings: number;
failFindings: number;
riskLevel: "low-high" | "high" | "critical";
riskLevel: RiskLevel;
severityData: Array<{
name: string;
value: number;
@@ -215,6 +223,44 @@ const MONGODBATLAS_COORDINATES: Record<string, { lat: number; lng: number }> = {
global: { lat: 40.8, lng: -74.0 }, // Global fallback
};
// Alibaba Cloud regions
const ALIBABACLOUD_COORDINATES: Record<string, { lat: number; lng: number }> = {
// China regions
"cn-hangzhou": { lat: 30.3, lng: 120.2 }, // Hangzhou
"cn-shanghai": { lat: 31.2, lng: 121.5 }, // Shanghai
"cn-beijing": { lat: 39.9, lng: 116.4 }, // Beijing
"cn-shenzhen": { lat: 22.5, lng: 114.1 }, // Shenzhen
"cn-zhangjiakou": { lat: 40.8, lng: 114.9 }, // Zhangjiakou
"cn-huhehaote": { lat: 40.8, lng: 111.7 }, // Hohhot
"cn-wulanchabu": { lat: 41.0, lng: 113.1 }, // Ulanqab
"cn-chengdu": { lat: 30.7, lng: 104.1 }, // Chengdu
"cn-qingdao": { lat: 36.1, lng: 120.4 }, // Qingdao
"cn-nanjing": { lat: 32.1, lng: 118.8 }, // Nanjing
"cn-fuzhou": { lat: 26.1, lng: 119.3 }, // Fuzhou
"cn-guangzhou": { lat: 23.1, lng: 113.3 }, // Guangzhou
"cn-heyuan": { lat: 23.7, lng: 114.7 }, // Heyuan
"cn-hongkong": { lat: 22.3, lng: 114.2 }, // Hong Kong
// Asia Pacific regions
"ap-southeast-1": { lat: 1.4, lng: 103.8 }, // Singapore
"ap-southeast-2": { lat: -33.9, lng: 151.2 }, // Sydney
"ap-southeast-3": { lat: 3.1, lng: 101.7 }, // Kuala Lumpur
"ap-southeast-5": { lat: -6.2, lng: 106.8 }, // Jakarta
"ap-southeast-6": { lat: 13.8, lng: 100.5 }, // Bangkok
"ap-southeast-7": { lat: 10.8, lng: 106.6 }, // Ho Chi Minh City
"ap-northeast-1": { lat: 35.7, lng: 139.7 }, // Tokyo
"ap-northeast-2": { lat: 37.6, lng: 127.0 }, // Seoul
"ap-south-1": { lat: 19.1, lng: 72.9 }, // Mumbai
// US & Europe regions
"us-west-1": { lat: 37.4, lng: -121.9 }, // Silicon Valley
"us-east-1": { lat: 39.0, lng: -77.5 }, // Virginia
"eu-west-1": { lat: 51.5, lng: -0.1 }, // London
"eu-central-1": { lat: 50.1, lng: 8.7 }, // Frankfurt
// Middle East regions
"me-east-1": { lat: 25.3, lng: 55.3 }, // Dubai
"me-central-1": { lat: 24.5, lng: 54.4 }, // Riyadh
global: { lat: 30.3, lng: 120.2 }, // Global fallback (Hangzhou HQ)
};
const PROVIDER_COORDINATES: Record<
string,
Record<string, { lat: number; lng: number }>
@@ -230,6 +276,7 @@ const PROVIDER_COORDINATES: Record<
iac: IAC_COORDINATES,
oraclecloud: ORACLECLOUD_COORDINATES,
mongodbatlas: MONGODBATLAS_COORDINATES,
alibabacloud: ALIBABACLOUD_COORDINATES,
};
// Returns [lng, lat] format for D3/GeoJSON compatibility
@@ -253,10 +300,10 @@ function getRegionCoordinates(
return coords ? [coords.lng, coords.lat] : null;
}
function getRiskLevel(failRate: number): "low-high" | "high" | "critical" {
if (failRate >= 0.5) return "critical";
if (failRate >= 0.25) return "high";
return "low-high";
function getRiskLevel(failRate: number): RiskLevel {
if (failRate >= 0.5) return RISK_LEVELS.CRITICAL;
if (failRate >= 0.25) return RISK_LEVELS.HIGH;
return RISK_LEVELS.LOW_HIGH;
}
// CSS variables are used for Recharts inline styles, not className
@@ -4,6 +4,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { ReactNode } from "react";
import {
AlibabaCloudProviderBadge,
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
@@ -33,6 +34,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
iac: <IacProviderBadge width={18} height={18} />,
oraclecloud: <OracleCloudProviderBadge width={18} height={18} />,
mongodbatlas: <MongoDBAtlasProviderBadge width={18} height={18} />,
alibabacloud: <AlibabaCloudProviderBadge width={18} height={18} />,
};
interface AccountsSelectorProps {
@@ -57,6 +57,11 @@ const MongoDBAtlasProviderBadge = lazy(() =>
default: m.MongoDBAtlasProviderBadge,
})),
);
const AlibabaCloudProviderBadge = lazy(() =>
import("@/components/icons/providers-badge").then((m) => ({
default: m.AlibabaCloudProviderBadge,
})),
);
type IconProps = { width: number; height: number };
@@ -104,6 +109,10 @@ const PROVIDER_DATA: Record<
label: "MongoDB Atlas",
icon: MongoDBAtlasProviderBadge,
},
alibabacloud: {
label: "Alibaba Cloud",
icon: AlibabaCloudProviderBadge,
},
};
type ProviderTypeSelectorProps = {
@@ -1,10 +1,9 @@
import React from "react";
import { getProvider } from "@/actions/providers/providers";
import {
AddViaCredentialsForm,
AddViaRoleForm,
} from "@/components/providers/workflow/forms";
import { SelectViaAlibabaCloud } from "@/components/providers/workflow/forms/select-credentials-type/alibabacloud";
import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws";
import {
AddViaServiceAccountForm,
@@ -42,6 +41,8 @@ export default async function AddCredentialsPage({ searchParams }: Props) {
if (providerType === "github")
return <SelectViaGitHub initialVia={via} />;
if (providerType === "m365") return <SelectViaM365 initialVia={via} />;
if (providerType === "alibabacloud")
return <SelectViaAlibabaCloud initialVia={via} />;
return null;
case "credentials":
@@ -1,6 +1,5 @@
import React from "react";
import {
AlibabaCloudProviderBadge,
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
@@ -92,3 +91,12 @@ export const CustomProviderInputOracleCloud = () => {
</div>
);
};
export const CustomProviderInputAlibabaCloud = () => {
return (
<div className="flex items-center gap-x-2">
<AlibabaCloudProviderBadge width={25} height={25} />
<p className="text-sm">Alibaba Cloud</p>
</div>
);
};
@@ -2,11 +2,12 @@
import { Select, SelectItem } from "@heroui/select";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useCallback, useMemo } from "react";
import { ReactElement } from "react";
import { PROVIDER_TYPES, ProviderType } from "@/types/providers";
import {
CustomProviderInputAlibabaCloud,
CustomProviderInputAWS,
CustomProviderInputAzure,
CustomProviderInputGCP,
@@ -20,7 +21,7 @@ import {
const providerDisplayData: Record<
ProviderType,
{ label: string; component: React.ReactElement }
{ label: string; component: ReactElement }
> = {
aws: {
label: "Amazon Web Services",
@@ -58,6 +59,10 @@ const providerDisplayData: Record<
label: "Oracle Cloud Infrastructure",
component: <CustomProviderInputOracleCloud />,
},
alibabacloud: {
label: "Alibaba Cloud",
component: <CustomProviderInputAlibabaCloud />,
},
};
const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({
@@ -66,32 +71,27 @@ const dataInputsProvider = PROVIDER_TYPES.map((providerType) => ({
value: providerDisplayData[providerType].component,
}));
export const CustomSelectProvider: React.FC = () => {
export const CustomSelectProvider = () => {
const router = useRouter();
const searchParams = useSearchParams();
const applyProviderFilter = useCallback(
(value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set("filter[provider_type]", value);
} else {
params.delete("filter[provider_type]");
}
router.push(`?${params.toString()}`, { scroll: false });
},
[router, searchParams],
);
const applyProviderFilter = (value: string) => {
const params = new URLSearchParams(searchParams.toString());
if (value) {
params.set("filter[provider_type]", value);
} else {
params.delete("filter[provider_type]");
}
router.push(`?${params.toString()}`, { scroll: false });
};
const currentProvider = searchParams.get("filter[provider_type]") || "";
const selectedKeys = useMemo(() => {
return dataInputsProvider.some(
(provider) => provider.key === currentProvider,
)
? [currentProvider]
: [];
}, [currentProvider]);
const selectedKeys = dataInputsProvider.some(
(provider) => provider.key === currentProvider,
)
? [currentProvider]
: [];
return (
<Select
@@ -0,0 +1,40 @@
import { FC } from "react";
import { IconSvgProps } from "@/types";
export const AlibabaCloudProviderBadge: 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(28, 66) scale(1.66)">
{/* Horizontal bar */}
<rect x="40.1" y="32.8" fill="#FF6A00" width="40.1" height="9" />
{/* Right bracket */}
<path
fill="#FF6A00"
d="M100.2,0H73.7l6.4,9.1L99.5,15c3.6,1.1,5.9,4.5,5.8,8c0,0,0,0,0,0V52c0,0,0,0,0,0c0,3.6-2.3,6.9-5.8,8l-19.3,5.9L73.7,75h26.5c11.1,0,20-9,20-20V20C120.3,9,111.3,0,100.2,0"
/>
{/* Left bracket */}
<path
fill="#FF6A00"
d="M20,0h26.5l-6.4,9.1L20.8,15c-3.6,1.1-5.9,4.5-5.8,8c0,0,0,0,0,0V52c0,0,0,0,0,0c0,3.6,2.3,6.9,5.8,8l19.3,5.9l6.4,9.1H20C9,75,0,66,0,55V20C0,9,9,0,20,0"
/>
</g>
</g>
</svg>
);
+6 -1
View File
@@ -1,5 +1,8 @@
import type { FC } from "react";
import { IconSvgProps } from "@/types";
import { AlibabaCloudProviderBadge } from "./alibabacloud-provider-badge";
import { AWSProviderBadge } from "./aws-provider-badge";
import { AzureProviderBadge } from "./azure-provider-badge";
import { GCPProviderBadge } from "./gcp-provider-badge";
@@ -11,6 +14,7 @@ import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge";
import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge";
export {
AlibabaCloudProviderBadge,
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
@@ -23,7 +27,7 @@ export {
};
// Map provider display names to their icon components
export const PROVIDER_ICONS: Record<string, React.FC<IconSvgProps>> = {
export const PROVIDER_ICONS: Record<string, FC<IconSvgProps>> = {
AWS: AWSProviderBadge,
Azure: AzureProviderBadge,
"Google Cloud": GCPProviderBadge,
@@ -33,4 +37,5 @@ export const PROVIDER_ICONS: Record<string, React.FC<IconSvgProps>> = {
"Infrastructure as Code": IacProviderBadge,
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,
"MongoDB Atlas": MongoDBAtlasProviderBadge,
"Alibaba Cloud": AlibabaCloudProviderBadge,
};
@@ -2,9 +2,10 @@
import { Input } from "@heroui/input";
import { Select, SelectItem } from "@heroui/select";
import { SharedSelection } from "@heroui/system";
import { CheckSquare, Search, Square } from "lucide-react";
import { useMemo, useState } from "react";
import { Control } from "react-hook-form";
import { useState } from "react";
import { Control, FieldValues, Path } from "react-hook-form";
import { Button } from "@/components/shadcn";
import { FormControl, FormField, FormMessage } from "@/components/ui/form";
@@ -20,11 +21,12 @@ const providerTypeLabels: Record<ProviderType, string> = {
iac: "Infrastructure as Code",
oraclecloud: "Oracle Cloud Infrastructure",
mongodbatlas: "MongoDB Atlas",
alibabacloud: "Alibaba Cloud",
};
interface EnhancedProviderSelectorProps {
control: Control<any>;
name: string;
interface EnhancedProviderSelectorProps<T extends FieldValues> {
control: Control<T>;
name: Path<T>;
providers: ProviderProps[];
label?: string;
placeholder?: string;
@@ -36,7 +38,7 @@ interface EnhancedProviderSelectorProps {
disabledProviderIds?: string[];
}
export const EnhancedProviderSelector = ({
export const EnhancedProviderSelector = <T extends FieldValues>({
control,
name,
providers,
@@ -48,10 +50,10 @@ export const EnhancedProviderSelector = ({
providerType,
enableSearch = false,
disabledProviderIds = [],
}: EnhancedProviderSelectorProps) => {
}: EnhancedProviderSelectorProps<T>) => {
const [searchValue, setSearchValue] = useState("");
const filteredProviders = useMemo(() => {
const filteredProviders = (() => {
let filtered = providers;
// Filter by provider type if specified
@@ -83,7 +85,7 @@ export const EnhancedProviderSelector = ({
const nameB = b.attributes.alias || b.attributes.uid;
return nameA.localeCompare(nameB);
});
}, [providers, providerType, searchValue, enableSearch]);
})();
return (
<FormField
@@ -91,7 +93,11 @@ export const EnhancedProviderSelector = ({
name={name}
render={({ field: { onChange, value, onBlur } }) => {
const isMultiple = selectionMode === "multiple";
const selectedIds = isMultiple ? value || [] : value ? [value] : [];
const selectedIds: string[] = isMultiple
? (value as string[] | undefined) || []
: value
? [value as string]
: [];
const allProviderIds = filteredProviders
.filter((p) => !disabledProviderIds.includes(p.id))
.map((p) => p.id);
@@ -108,13 +114,17 @@ export const EnhancedProviderSelector = ({
}
};
const handleSelectionChange = (keys: any) => {
const handleSelectionChange = (keys: SharedSelection) => {
if (keys === "all") {
onChange(allProviderIds);
return;
}
if (isMultiple) {
const selectedArray = Array.from(keys);
const selectedArray = Array.from(keys).map(String);
onChange(selectedArray);
} else {
const selectedValue = Array.from(keys)[0];
onChange(selectedValue || "");
onChange(selectedValue ? String(selectedValue) : "");
}
};
@@ -1,13 +1,14 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import React from "react";
import { FC } from "react";
import { Control, Controller } from "react-hook-form";
import { z } from "zod";
import { addProviderFormSchema } from "@/types";
import {
AlibabaCloudProviderBadge,
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
@@ -27,7 +28,7 @@ interface RadioGroupProviderProps {
errorMessage?: string;
}
export const RadioGroupProvider: React.FC<RadioGroupProviderProps> = ({
export const RadioGroupProvider: FC<RadioGroupProviderProps> = ({
control,
isInvalid,
errorMessage,
@@ -102,6 +103,12 @@ export const RadioGroupProvider: React.FC<RadioGroupProviderProps> = ({
<span className="ml-2">Oracle Cloud Infrastructure</span>
</div>
</CustomRadio>
<CustomRadio description="Alibaba Cloud" value="alibabacloud">
<div className="flex items-center">
<AlibabaCloudProviderBadge size={26} />
<span className="ml-2">Alibaba Cloud</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (
@@ -2,7 +2,7 @@
import { Divider } from "@heroui/divider";
import { ChevronLeftIcon, ChevronRightIcon, Loader2 } from "lucide-react";
import { Control } from "react-hook-form";
import { Control, UseFormSetValue } from "react-hook-form";
import { Button } from "@/components/shadcn";
import { Form } from "@/components/ui/form";
@@ -11,6 +11,9 @@ import { getAWSCredentialsTemplateLinks } from "@/lib";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { requiresBackButton } from "@/lib/provider-helpers";
import {
AlibabaCloudCredentials,
AlibabaCloudCredentialsRole,
ApiError,
AWSCredentials,
AWSCredentialsRole,
AzureCredentials,
@@ -26,6 +29,10 @@ import {
} from "@/types";
import { ProviderTitleDocs } from "../provider-title-docs";
import {
AlibabaCloudRoleCredentialsForm,
AlibabaCloudStaticCredentialsForm,
} from "./select-credentials-type/alibabacloud/credentials-type";
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";
@@ -41,11 +48,19 @@ import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-for
import { MongoDBAtlasCredentialsForm } from "./via-credentials/mongodbatlas-credentials-form";
import { OracleCloudCredentialsForm } from "./via-credentials/oraclecloud-credentials-form";
type ApiResponse = {
error?: string;
errors?: ApiError[];
data?: unknown;
success?: boolean;
status?: number;
};
type BaseCredentialsFormProps = {
providerType: ProviderType;
providerId: string;
providerUid?: string;
onSubmit: (formData: FormData) => Promise<any>;
onSubmit: (formData: FormData) => Promise<ApiResponse>;
successNavigationUrl: string;
submitButtonText?: string;
showBackButton?: boolean;
@@ -108,7 +123,9 @@ export const BaseCredentialsForm = ({
{providerType === "aws" && searchParamsObj.get("via") === "role" && (
<AWSRoleCredentialsForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={form.setValue as any}
setValue={
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
}
externalId={externalId}
templateLinks={templateLinks}
/>
@@ -181,6 +198,22 @@ export const BaseCredentialsForm = ({
}
/>
)}
{providerType === "alibabacloud" &&
searchParamsObj.get("via") === "role" && (
<AlibabaCloudRoleCredentialsForm
control={
form.control as unknown as Control<AlibabaCloudCredentialsRole>
}
/>
)}
{providerType === "alibabacloud" &&
searchParamsObj.get("via") !== "role" && (
<AlibabaCloudStaticCredentialsForm
control={
form.control as unknown as Control<AlibabaCloudCredentials>
}
/>
)}
<div className="flex w-full justify-end gap-4">
{showBackButton && requiresBackButton(searchParamsObj.get("via")) && (
@@ -5,7 +5,7 @@ import { ChevronLeftIcon, ChevronRightIcon, Loader2 } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import { z } from "zod";
import { addProvider } from "@/actions/providers/providers";
import { ProviderTitleDocs } from "@/components/providers/workflow/provider-title-docs";
@@ -67,6 +67,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
label: "Organization ID",
placeholder: "e.g. 5f43a8c4e1234567890abcde",
};
case "alibabacloud":
return {
label: "Account ID",
placeholder: "e.g. 1234567890123456",
};
default:
return {
label: "Provider UID",
@@ -151,13 +156,16 @@ export const ConnectAccountForm = () => {
router.push(`/providers/add-credentials?type=${providerType}&id=${id}`);
}
} catch (error: any) {
} catch (error: unknown) {
// eslint-disable-next-line no-console
console.error("Error during submission:", error);
toast({
variant: "destructive",
title: "Submission Error",
description: error.message || "Something went wrong. Please try again.",
description:
error instanceof Error
? error.message
: "Something went wrong. Please try again.",
});
}
};
@@ -0,0 +1,87 @@
import { Divider } from "@heroui/divider";
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AlibabaCloudCredentialsRole } from "@/types";
export const AlibabaCloudRoleCredentialsForm = ({
control,
}: {
control: Control<AlibabaCloudCredentialsRole>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
Connect assuming RAM Role
</div>
<div className="text-default-500 text-sm">
Provide the RAM Role ARN to assume, along with the Access Keys of a
RAM user that has permission to assume the role.
</div>
</div>
<span className="text-default-500 text-xs font-bold">
RAM Role to Assume
</span>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN}
type="text"
label="Role ARN"
labelPlacement="inside"
placeholder="e.g. acs:ram::1234567890123456:role/ProwlerRole"
variant="bordered"
isRequired
/>
<Divider />
<span className="text-default-500 text-xs font-bold">
Credentials for Role Assumption
</span>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID}
type="text"
label="Access Key ID"
labelPlacement="inside"
placeholder="e.g. LTAI5txxxxxxxxxx"
variant="bordered"
isRequired
/>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET}
type="password"
label="Access Key Secret"
labelPlacement="inside"
placeholder="Enter the access key secret"
variant="bordered"
isRequired
/>
<span className="text-default-500 text-xs">Optional fields</span>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME}
type="text"
label="Role Session Name"
labelPlacement="inside"
placeholder="Enter the role session name (default: ProwlerSession)"
variant="bordered"
isRequired={false}
/>
<div className="text-default-400 text-xs">
Keys never leave your browser unencrypted and are stored as secrets in
the backend. The role will be assumed using STS to obtain temporary
credentials.
</div>
</>
);
};
@@ -0,0 +1,50 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AlibabaCloudCredentials } from "@/types";
export const AlibabaCloudStaticCredentialsForm = ({
control,
}: {
control: Control<AlibabaCloudCredentials>;
}) => {
return (
<>
<div className="flex flex-col">
<div className="text-md text-default-foreground leading-9 font-bold">
Connect via Access Keys
</div>
<div className="text-default-500 text-sm">
Provide a RAM user Access Key ID and Access Key Secret with read
access to the resources you want Prowler to assess.
</div>
</div>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID}
type="text"
label="Access Key ID"
labelPlacement="inside"
placeholder="e.g. LTAI5txxxxxxxxxx"
variant="bordered"
isRequired
/>
<CustomInput
control={control}
name={ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET}
type="password"
label="Access Key Secret"
labelPlacement="inside"
placeholder="Enter the access key secret"
variant="bordered"
isRequired
/>
<div className="text-default-400 text-xs">
Keys never leave your browser unencrypted and are stored as secrets in
the backend. Rotate the key from Alibaba Cloud RAM console anytime if
needed.
</div>
</>
);
};
@@ -0,0 +1,2 @@
export * from "./alibabacloud-role-credentials-form";
export * from "./alibabacloud-static-credentials-form";
@@ -0,0 +1,2 @@
export * from "./radio-group-alibabacloud-via-credentials-type-form";
export * from "./select-via-alibabacloud";
@@ -0,0 +1,71 @@
"use client";
import { RadioGroup } from "@heroui/radio";
import { Control, Controller, FieldValues, Path } from "react-hook-form";
import { CustomRadio } from "@/components/ui/custom";
import { FormMessage } from "@/components/ui/form";
type RadioGroupAlibabaCloudViaCredentialsFormProps<T extends FieldValues> = {
control: Control<T>;
isInvalid: boolean;
errorMessage?: string;
onChange?: (value: string) => void;
};
export const RadioGroupAlibabaCloudViaCredentialsTypeForm = <
T extends FieldValues,
>({
control,
isInvalid,
errorMessage,
onChange,
}: RadioGroupAlibabaCloudViaCredentialsFormProps<T>) => {
return (
<Controller
name={"alibabacloudCredentialsType" as Path<T>}
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">Using RAM Role</span>
<CustomRadio description="Connect assuming RAM Role" value="role">
<div className="flex items-center">
<span className="ml-2">Connect assuming RAM Role</span>
</div>
</CustomRadio>
<span className="text-default-500 text-sm">
Using Credentials
</span>
<CustomRadio
description="Connect via Access Keys"
value="credentials"
>
<div className="flex items-center">
<span className="ml-2">Connect via Access Keys</span>
</div>
</CustomRadio>
</div>
</RadioGroup>
{errorMessage && (
<FormMessage className="text-text-error">
{errorMessage}
</FormMessage>
)}
</>
)}
/>
);
};
@@ -0,0 +1,42 @@
"use client";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { Form } from "@/components/ui/form";
import { RadioGroupAlibabaCloudViaCredentialsTypeForm } from "./radio-group-alibabacloud-via-credentials-type-form";
interface SelectViaAlibabaCloudProps {
initialVia?: string;
}
export const SelectViaAlibabaCloud = ({
initialVia,
}: SelectViaAlibabaCloudProps) => {
const router = useRouter();
const form = useForm({
defaultValues: {
alibabacloudCredentialsType: initialVia || "",
},
});
const handleSelectionChange = (value: string) => {
const url = new URL(window.location.href);
url.searchParams.set("via", value);
router.push(url.toString());
};
return (
<Form {...form}>
<RadioGroupAlibabaCloudViaCredentialsTypeForm
control={form.control}
isInvalid={!!form.formState.errors.alibabacloudCredentialsType}
errorMessage={
form.formState.errors.alibabacloudCredentialsType?.message as string
}
onChange={handleSelectionChange}
/>
</Form>
);
};
@@ -1,6 +1,7 @@
import React from "react";
"use client";
import {
AlibabaCloudProviderBadge,
AWSProviderBadge,
AzureProviderBadge,
GCPProviderBadge,
@@ -33,6 +34,8 @@ export const getProviderLogo = (provider: ProviderType) => {
return <OracleCloudProviderBadge width={35} height={35} />;
case "mongodbatlas":
return <MongoDBAtlasProviderBadge width={35} height={35} />;
case "alibabacloud":
return <AlibabaCloudProviderBadge width={35} height={35} />;
default:
return null;
}
@@ -58,6 +61,8 @@ export const getProviderName = (provider: ProviderType): string => {
return "Oracle Cloud Infrastructure";
case "mongodbatlas":
return "MongoDB Atlas";
case "alibabacloud":
return "Alibaba Cloud";
default:
return "Unknown Provider";
}
+29 -1
View File
@@ -11,14 +11,23 @@ import {
addCredentialsFormSchema,
addCredentialsRoleFormSchema,
addCredentialsServiceAccountFormSchema,
ApiError,
ProviderType,
} from "@/types";
type ApiResponse = {
error?: string;
errors?: ApiError[];
data?: unknown;
success?: boolean;
status?: number;
};
type UseCredentialsFormProps = {
providerType: ProviderType;
providerId: string;
providerUid?: string;
onSubmit: (formData: FormData) => Promise<any>;
onSubmit: (formData: FormData) => Promise<ApiResponse>;
successNavigationUrl: string;
};
@@ -39,6 +48,9 @@ export const useCredentialsForm = ({
if (providerType === "aws" && via === "role") {
return addCredentialsRoleFormSchema(providerType);
}
if (providerType === "alibabacloud" && via === "role") {
return addCredentialsRoleFormSchema(providerType);
}
if (providerType === "gcp" && via === "service-account") {
return addCredentialsServiceAccountFormSchema(providerType);
}
@@ -173,6 +185,22 @@ export const useCredentialsForm = ({
[ProviderCredentialFields.ATLAS_PUBLIC_KEY]: "",
[ProviderCredentialFields.ATLAS_PRIVATE_KEY]: "",
};
case "alibabacloud":
// AlibabaCloud Role credentials
if (via === "role") {
return {
...baseDefaults,
[ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN]: "",
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: "",
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: "",
[ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME]: "",
};
}
return {
...baseDefaults,
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: "",
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: "",
};
default:
return baseDefaults;
}
+5
View File
@@ -47,6 +47,11 @@ export const getProviderHelpText = (provider: string) => {
text: "Need help connecting your MongoDB Atlas organization?",
link: "https://goto.prowler.com/provider-mongodbatlas",
};
case "alibabacloud":
return {
text: "Need help connecting your Alibaba Cloud account?",
link: "https://goto.prowler.com/provider-alibabacloud",
};
default:
return {
text: "How to setup a provider?",
@@ -211,6 +211,45 @@ export const buildMongoDBAtlasSecret = (formData: FormData) => {
return filterEmptyValues(secret);
};
export const buildAlibabaCloudSecret = (
formData: FormData,
isRole: boolean,
) => {
if (isRole) {
const secret = {
[ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN,
),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID,
),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET,
),
[ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME,
),
};
return filterEmptyValues(secret);
}
const secret = {
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID,
),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: getFormValue(
formData,
ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET,
),
};
return filterEmptyValues(secret);
};
export const buildIacSecret = (formData: FormData) => {
const secret = {
[ProviderCredentialFields.REPOSITORY_URL]: getFormValue(
@@ -326,6 +365,14 @@ export const buildSecretConfig = (
secretType: "static",
secret: buildMongoDBAtlasSecret(formData),
}),
alibabacloud: () => {
const isRole =
formData.get(ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN) !== null;
return {
secretType: isRole ? "role" : "static",
secret: buildAlibabaCloudSecret(formData, isRole),
};
},
};
const builder = secretBuilders[providerType];
@@ -61,6 +61,12 @@ export const ProviderCredentialFields = {
OCI_TENANCY: "tenancy",
OCI_REGION: "region",
OCI_PASS_PHRASE: "pass_phrase",
// Alibaba Cloud fields
ALIBABACLOUD_ACCESS_KEY_ID: "access_key_id",
ALIBABACLOUD_ACCESS_KEY_SECRET: "access_key_secret",
ALIBABACLOUD_ROLE_ARN: "role_arn",
ALIBABACLOUD_ROLE_SESSION_NAME: "role_session_name",
} as const;
// Type for credential field values
@@ -101,6 +107,10 @@ export const ErrorPointers = {
OCI_PASS_PHRASE: "/data/attributes/secret/pass_phrase",
ATLAS_PUBLIC_KEY: "/data/attributes/secret/atlas_public_key",
ATLAS_PRIVATE_KEY: "/data/attributes/secret/atlas_private_key",
ALIBABACLOUD_ACCESS_KEY_ID: "/data/attributes/secret/access_key_id",
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",
} as const;
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
+14 -1
View File
@@ -122,7 +122,13 @@ export const getProviderFormType = (
via?: string,
): ProviderFormType => {
// Providers that need credential type selection
const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType);
const needsSelector = [
"aws",
"gcp",
"github",
"m365",
"alibabacloud",
].includes(providerType);
// Show selector if no via parameter and provider needs it
if (needsSelector && !via) {
@@ -157,6 +163,12 @@ export const getProviderFormType = (
return "credentials";
}
// AlibabaCloud specific forms
if (providerType === "alibabacloud") {
if (via === "role") return "role";
if (via === "credentials") return "credentials";
}
// Other providers go directly to credentials form
if (!needsSelector) {
return "credentials";
@@ -179,6 +191,7 @@ export const requiresBackButton = (via?: string | null): boolean => {
"app_client_secret",
"app_certificate",
];
// Note: "role" is already included for AWS, now also used by AlibabaCloud
return validViaTypes.includes(via);
};
+17 -1
View File
@@ -270,6 +270,20 @@ export type MongoDBAtlasCredentials = {
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type AlibabaCloudCredentials = {
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: string;
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type AlibabaCloudCredentialsRole = {
[ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN]: string;
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: string;
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: string;
[ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME]?: string;
[ProviderCredentialFields.PROVIDER_ID]: string;
};
export type CredentialsFormSchema =
| AWSCredentials
| AzureCredentials
@@ -279,7 +293,9 @@ export type CredentialsFormSchema =
| IacCredentials
| M365Credentials
| OCICredentials
| MongoDBAtlasCredentials;
| MongoDBAtlasCredentials
| AlibabaCloudCredentials
| AlibabaCloudCredentialsRole;
export interface SearchParamsProps {
[key: string]: string | string[] | undefined;
+37 -6
View File
@@ -125,6 +125,11 @@ export const addProviderFormSchema = z
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
z.object({
providerType: z.literal("alibabacloud"),
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
providerUid: z.string(),
}),
]),
);
@@ -245,9 +250,18 @@ export const addCredentialsFormSchema = (
.string()
.min(1, "Atlas Private Key is required"),
}
: {}),
: providerType === "alibabacloud"
? {
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]:
z.string().min(1, "Access Key ID is required"),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]:
z
.string()
.min(1, "Access Key Secret is required"),
}
: {}),
})
.superRefine((data: Record<string, any>, ctx) => {
.superRefine((data: Record<string, string | undefined>, ctx) => {
if (providerType === "m365") {
// Validate based on the via parameter
if (via === "app_client_secret") {
@@ -339,10 +353,27 @@ export const addCredentialsRoleFormSchema = (providerType: string) =>
path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID],
},
)
: z.object({
providerId: z.string(),
providerType: z.string(),
});
: providerType === "alibabacloud"
? z.object({
[ProviderCredentialFields.PROVIDER_ID]: z.string(),
[ProviderCredentialFields.PROVIDER_TYPE]: z.string(),
[ProviderCredentialFields.ALIBABACLOUD_ROLE_ARN]: z
.string()
.min(1, "RAM Role ARN is required"),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_ID]: z
.string()
.min(1, "Access Key ID is required"),
[ProviderCredentialFields.ALIBABACLOUD_ACCESS_KEY_SECRET]: z
.string()
.min(1, "Access Key Secret is required"),
[ProviderCredentialFields.ALIBABACLOUD_ROLE_SESSION_NAME]: z
.string()
.optional(),
})
: z.object({
providerId: z.string(),
providerType: z.string(),
});
export const addCredentialsServiceAccountFormSchema = (
providerType: ProviderType,
+2
View File
@@ -8,6 +8,7 @@ export const PROVIDER_TYPES = [
"github",
"iac",
"oraclecloud",
"alibabacloud",
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];
@@ -22,6 +23,7 @@ export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
github: "GitHub",
iac: "Infrastructure as Code",
oraclecloud: "Oracle Cloud Infrastructure",
alibabacloud: "Alibaba Cloud",
};
export function getProviderDisplayName(providerId: string): string {