mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-31 07:44:21 +00:00
feat: add Lovable AI app builder provider
- Add lovable SDK provider with apps and published services - Add 12 security checks mapped to Lovable security best practices - Add Prowler ThreatScore for Lovable compliance framework - Add API support: provider type, secret schema, UID validator, migration - Add UI support: provider choice, credentials form, badges, wiring - Add SDK tests for provider, services, and critical checks
This commit is contained in:
1 parent
34727a7237
commit
077a4e7dab
100 files changed
+3106
-4
No files matched your search
@@ -4,6 +4,10 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
## [1.26.0] (Prowler UNRELEASED)
|
||||
|
||||
### 🚀 Added
|
||||
|
||||
- Lovable provider option in the connect-account flow: provider radio choice, brand badge, workspace UID input, credentials form (Lovable API token plus optional Supabase access token), and findings/overview wiring
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
- Standardized "Providers" wording across UI and documentation, replacing legacy "Cloud Providers" / "Accounts" / "Account Groups" copy [(#10971)](https://github.com/prowler-cloud/prowler/pull/10971)
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({
|
||||
OracleCloudProviderBadge: () => <span>Oracle Cloud</span>,
|
||||
AlibabaCloudProviderBadge: () => <span>Alibaba Cloud</span>,
|
||||
VercelProviderBadge: () => <span>Vercel</span>,
|
||||
LovableProviderBadge: () => <span>Lovable</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/select/multiselect", () => ({
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
IacProviderBadge,
|
||||
ImageProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
LovableProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
@@ -51,6 +52,7 @@ const PROVIDER_ICON: Record<ProviderType, ReactNode> = {
|
||||
cloudflare: <CloudflareProviderBadge width={18} height={18} />,
|
||||
openstack: <OpenStackProviderBadge width={18} height={18} />,
|
||||
vercel: <VercelProviderBadge width={18} height={18} />,
|
||||
lovable: <LovableProviderBadge width={18} height={18} />,
|
||||
};
|
||||
|
||||
/** Common props shared by both batch and instant modes. */
|
||||
|
||||
@@ -31,6 +31,7 @@ vi.mock("@/components/icons/providers-badge", () => ({
|
||||
CloudflareProviderBadge: () => <span>Cloudflare</span>,
|
||||
OpenStackProviderBadge: () => <span>OpenStack</span>,
|
||||
VercelProviderBadge: () => <span>Vercel</span>,
|
||||
LovableProviderBadge: () => <span>Lovable</span>,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/shadcn/select/multiselect", () => ({
|
||||
|
||||
@@ -89,6 +89,11 @@ const VercelProviderBadge = lazy(() =>
|
||||
default: m.VercelProviderBadge,
|
||||
})),
|
||||
);
|
||||
const LovableProviderBadge = lazy(() =>
|
||||
import("@/components/icons/providers-badge").then((m) => ({
|
||||
default: m.LovableProviderBadge,
|
||||
})),
|
||||
);
|
||||
|
||||
type IconProps = { width: number; height: number };
|
||||
|
||||
@@ -160,6 +165,10 @@ const PROVIDER_DATA: Record<
|
||||
label: "Vercel",
|
||||
icon: VercelProviderBadge,
|
||||
},
|
||||
lovable: {
|
||||
label: "Lovable",
|
||||
icon: LovableProviderBadge,
|
||||
},
|
||||
};
|
||||
|
||||
/** Common props shared by both batch and instant modes. */
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IacProviderBadge,
|
||||
ImageProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
LovableProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
@@ -34,6 +35,7 @@ export const PROVIDER_ICONS = {
|
||||
cloudflare: CloudflareProviderBadge,
|
||||
openstack: OpenStackProviderBadge,
|
||||
vercel: VercelProviderBadge,
|
||||
lovable: LovableProviderBadge,
|
||||
} as const;
|
||||
|
||||
interface ProviderIconCellProps {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { GoogleWorkspaceProviderBadge } from "./googleworkspace-provider-badge";
|
||||
import { IacProviderBadge } from "./iac-provider-badge";
|
||||
import { ImageProviderBadge } from "./image-provider-badge";
|
||||
import { KS8ProviderBadge } from "./ks8-provider-badge";
|
||||
import { LovableProviderBadge } from "./lovable-provider-badge";
|
||||
import { M365ProviderBadge } from "./m365-provider-badge";
|
||||
import { MongoDBAtlasProviderBadge } from "./mongodbatlas-provider-badge";
|
||||
import { OpenStackProviderBadge } from "./openstack-provider-badge";
|
||||
@@ -29,6 +30,7 @@ export {
|
||||
IacProviderBadge,
|
||||
ImageProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
LovableProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
@@ -53,4 +55,5 @@ export const PROVIDER_BADGE_BY_NAME: Record<string, FC<IconSvgProps>> = {
|
||||
Cloudflare: CloudflareProviderBadge,
|
||||
OpenStack: OpenStackProviderBadge,
|
||||
Vercel: VercelProviderBadge,
|
||||
Lovable: LovableProviderBadge,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
export const LovableProviderBadge: 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}
|
||||
>
|
||||
<rect width="256" height="256" fill="#FF4F8B" rx="60" />
|
||||
<path
|
||||
d="M128 196c-7 0-13.6-2.7-18.6-7.7l-44.7-44.7c-15.6-15.6-15.6-40.9 0-56.6 15.6-15.6 40.9-15.6 56.6 0L128 94.7l6.7-6.7c15.6-15.6 40.9-15.6 56.6 0 15.6 15.6 15.6 40.9 0 56.6l-44.7 44.7c-5 5-11.6 7.7-18.6 7.7Z"
|
||||
fill="#FFFFFF"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
IacProviderBadge,
|
||||
ImageProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
LovableProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
@@ -103,6 +104,11 @@ const PROVIDERS = [
|
||||
label: "Vercel",
|
||||
badge: VercelProviderBadge,
|
||||
},
|
||||
{
|
||||
value: "lovable",
|
||||
label: "Lovable",
|
||||
badge: LovableProviderBadge,
|
||||
},
|
||||
] as const;
|
||||
|
||||
interface RadioGroupProviderProps {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
IacCredentials,
|
||||
ImageCredentials,
|
||||
KubernetesCredentials,
|
||||
LovableCredentials,
|
||||
M365CertificateCredentials,
|
||||
M365ClientSecretCredentials,
|
||||
MongoDBAtlasCredentials,
|
||||
@@ -58,6 +59,7 @@ import { GoogleWorkspaceCredentialsForm } from "./via-credentials/googleworkspac
|
||||
import { IacCredentialsForm } from "./via-credentials/iac-credentials-form";
|
||||
import { ImageCredentialsForm } from "./via-credentials/image-credentials-form";
|
||||
import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form";
|
||||
import { LovableCredentialsForm } from "./via-credentials/lovable-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";
|
||||
@@ -279,6 +281,11 @@ export const BaseCredentialsForm = ({
|
||||
control={form.control as unknown as Control<VercelCredentials>}
|
||||
/>
|
||||
)}
|
||||
{providerType === "lovable" && (
|
||||
<LovableCredentialsForm
|
||||
control={form.control as unknown as Control<LovableCredentials>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideActions && (
|
||||
<div className="flex w-full justify-end gap-4">
|
||||
|
||||
@@ -121,6 +121,11 @@ const getProviderFieldDetails = (providerType?: ProviderType) => {
|
||||
label: "Team ID",
|
||||
placeholder: "e.g. team_xxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
};
|
||||
case "lovable":
|
||||
return {
|
||||
label: "Workspace ID",
|
||||
placeholder: "e.g. my-workspace or ws_xxxxxxxx",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: "Provider UID",
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { WizardInputField } from "@/components/providers/workflow/forms/fields";
|
||||
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
|
||||
import { LovableCredentials } from "@/types";
|
||||
|
||||
export const LovableCredentialsForm = ({
|
||||
control,
|
||||
}: {
|
||||
control: Control<LovableCredentials>;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Connect via API Token
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
Provide a Lovable Cloud API Token with read access to the workspace
|
||||
and projects you want Prowler to assess.
|
||||
</div>
|
||||
</div>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.LOVABLE_API_TOKEN}
|
||||
type="password"
|
||||
label="Lovable API Token"
|
||||
labelPlacement="inside"
|
||||
placeholder="Enter your Lovable Cloud API Token"
|
||||
variant="bordered"
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="text-md text-default-foreground leading-9 font-bold">
|
||||
Optional: Supabase access token
|
||||
</div>
|
||||
<div className="text-default-500 text-sm">
|
||||
If your Lovable apps are backed by Supabase, supply a Supabase
|
||||
Management API token to enable deeper checks (RLS posture, Edge
|
||||
Function authentication, storage privacy).
|
||||
</div>
|
||||
</div>
|
||||
<WizardInputField
|
||||
control={control}
|
||||
name={ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN}
|
||||
type="password"
|
||||
label="Supabase Access Token (optional)"
|
||||
labelPlacement="inside"
|
||||
placeholder="sbp_..."
|
||||
variant="bordered"
|
||||
/>
|
||||
|
||||
<div className="text-default-400 text-xs">
|
||||
Tokens never leave your browser unencrypted and are stored as secrets in
|
||||
the backend. You can revoke either token from the relevant dashboard at
|
||||
any time.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
IacProviderBadge,
|
||||
ImageProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
LovableProviderBadge,
|
||||
M365ProviderBadge,
|
||||
MongoDBAtlasProviderBadge,
|
||||
OpenStackProviderBadge,
|
||||
@@ -49,6 +50,8 @@ export const getProviderLogo = (provider: ProviderType) => {
|
||||
return <OpenStackProviderBadge width={35} height={35} />;
|
||||
case "vercel":
|
||||
return <VercelProviderBadge width={35} height={35} />;
|
||||
case "lovable":
|
||||
return <LovableProviderBadge width={35} height={35} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -86,6 +89,8 @@ export const getProviderName = (provider: ProviderType): string => {
|
||||
return "OpenStack";
|
||||
case "vercel":
|
||||
return "Vercel";
|
||||
case "lovable":
|
||||
return "Lovable";
|
||||
default:
|
||||
return "Unknown Provider";
|
||||
}
|
||||
|
||||
@@ -248,6 +248,12 @@ export const useCredentialsForm = ({
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.VERCEL_API_TOKEN]: "",
|
||||
};
|
||||
case "lovable":
|
||||
return {
|
||||
...baseDefaults,
|
||||
[ProviderCredentialFields.LOVABLE_API_TOKEN]: "",
|
||||
[ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN]: "",
|
||||
};
|
||||
default:
|
||||
return baseDefaults;
|
||||
}
|
||||
|
||||
@@ -100,6 +100,11 @@ export const getProviderHelpText = (provider: string) => {
|
||||
text: "Need help connecting your Vercel team?",
|
||||
link: "https://goto.prowler.com/provider-vercel",
|
||||
};
|
||||
case "lovable":
|
||||
return {
|
||||
text: "Need help connecting your Lovable workspace?",
|
||||
link: "https://docs.lovable.dev/tips-tricks/security-best-practices",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: "How to setup a provider?",
|
||||
|
||||
@@ -260,6 +260,20 @@ export const buildVercelSecret = (formData: FormData) => {
|
||||
return filterEmptyValues(secret);
|
||||
};
|
||||
|
||||
export const buildLovableSecret = (formData: FormData) => {
|
||||
const secret = {
|
||||
[ProviderCredentialFields.LOVABLE_API_TOKEN]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.LOVABLE_API_TOKEN,
|
||||
),
|
||||
[ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN]: getFormValue(
|
||||
formData,
|
||||
ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN,
|
||||
),
|
||||
};
|
||||
return filterEmptyValues(secret);
|
||||
};
|
||||
|
||||
export const buildOpenStackSecret = (formData: FormData) => {
|
||||
const secret = {
|
||||
[ProviderCredentialFields.OPENSTACK_CLOUDS_YAML_CONTENT]: getFormValue(
|
||||
@@ -513,6 +527,10 @@ export const buildSecretConfig = (
|
||||
secretType: "static",
|
||||
secret: buildVercelSecret(formData),
|
||||
}),
|
||||
lovable: () => ({
|
||||
secretType: "static",
|
||||
secret: buildLovableSecret(formData),
|
||||
}),
|
||||
};
|
||||
|
||||
const builder = secretBuilders[providerType];
|
||||
|
||||
@@ -91,6 +91,10 @@ export const ProviderCredentialFields = {
|
||||
|
||||
// Vercel fields
|
||||
VERCEL_API_TOKEN: "api_token",
|
||||
|
||||
// Lovable fields
|
||||
LOVABLE_API_TOKEN: "api_token",
|
||||
LOVABLE_SUPABASE_ACCESS_TOKEN: "supabase_access_token",
|
||||
} as const;
|
||||
|
||||
// Type for credential field values
|
||||
@@ -150,6 +154,9 @@ export const ErrorPointers = {
|
||||
"/data/attributes/secret/credentials_content",
|
||||
GOOGLEWORKSPACE_DELEGATED_USER: "/data/attributes/secret/delegated_user",
|
||||
VERCEL_API_TOKEN: "/data/attributes/secret/api_token",
|
||||
LOVABLE_API_TOKEN: "/data/attributes/secret/api_token",
|
||||
LOVABLE_SUPABASE_ACCESS_TOKEN:
|
||||
"/data/attributes/secret/supabase_access_token",
|
||||
} as const;
|
||||
|
||||
export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers];
|
||||
@@ -389,6 +389,12 @@ export type VercelCredentials = {
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type LovableCredentials = {
|
||||
[ProviderCredentialFields.LOVABLE_API_TOKEN]: string;
|
||||
[ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN]?: string;
|
||||
[ProviderCredentialFields.PROVIDER_ID]: string;
|
||||
};
|
||||
|
||||
export type CredentialsFormSchema =
|
||||
| AWSCredentials
|
||||
| AWSCredentialsRole
|
||||
@@ -406,7 +412,8 @@ export type CredentialsFormSchema =
|
||||
| CloudflareCredentials
|
||||
| OpenStackCredentials
|
||||
| GoogleWorkspaceCredentials
|
||||
| VercelCredentials;
|
||||
| VercelCredentials
|
||||
| LovableCredentials;
|
||||
|
||||
export interface SearchParamsProps {
|
||||
[key: string]: string | string[] | undefined;
|
||||
|
||||
+25
-1
@@ -163,6 +163,17 @@ export const addProviderFormSchema = z
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z.string().trim().min(1, "Team ID is required"),
|
||||
}),
|
||||
z.object({
|
||||
providerType: z.literal("lovable"),
|
||||
[ProviderCredentialFields.PROVIDER_ALIAS]: z.string(),
|
||||
providerUid: z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9][a-zA-Z0-9_-]{2,63}$/,
|
||||
"Workspace ID must be 3-64 alphanumeric characters, dashes, or underscores",
|
||||
),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
@@ -391,7 +402,20 @@ export const addCredentialsFormSchema = (
|
||||
.trim()
|
||||
.min(1, "API Token is required"),
|
||||
}
|
||||
: {}),
|
||||
: providerType === "lovable"
|
||||
? {
|
||||
[ProviderCredentialFields.LOVABLE_API_TOKEN]:
|
||||
z
|
||||
.string()
|
||||
.trim()
|
||||
.min(
|
||||
1,
|
||||
"Lovable API Token is required",
|
||||
),
|
||||
[ProviderCredentialFields.LOVABLE_SUPABASE_ACCESS_TOKEN]:
|
||||
z.string().trim().optional(),
|
||||
}
|
||||
: {}),
|
||||
})
|
||||
.superRefine((data: Record<string, string | undefined>, ctx) => {
|
||||
if (providerType === "m365") {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const PROVIDER_TYPES = [
|
||||
"cloudflare",
|
||||
"openstack",
|
||||
"vercel",
|
||||
"lovable",
|
||||
] as const;
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
@@ -34,6 +35,7 @@ export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
|
||||
cloudflare: "Cloudflare",
|
||||
openstack: "OpenStack",
|
||||
vercel: "Vercel",
|
||||
lovable: "Lovable",
|
||||
};
|
||||
|
||||
export function getProviderDisplayName(providerId: string): string {
|
||||
|
||||
Reference in new issue
Block a user