diff --git a/ui/components/users/profile/api-key-modals.integration.test.tsx b/ui/components/users/profile/api-key-modals.integration.test.tsx
new file mode 100644
index 0000000000..bf89021934
--- /dev/null
+++ b/ui/components/users/profile/api-key-modals.integration.test.tsx
@@ -0,0 +1,63 @@
+import { describe, expect, vi } from "vitest";
+
+import { it } from "@/__tests__/fixtures";
+import { render } from "@/__tests__/render-browser";
+
+import type { EnrichedApiKey } from "./api-keys/types";
+import { CreateApiKeyModal } from "./create-api-key-modal";
+import { EditApiKeyNameModal } from "./edit-api-key-name-modal";
+
+const apiKey: EnrichedApiKey = {
+ type: "api-keys",
+ id: "api-key-1",
+ attributes: {
+ name: "CI pipeline",
+ prefix: "pk_12345678",
+ expires_at: "2099-01-01T00:00:00Z",
+ revoked: false,
+ inserted_at: "2026-01-01T00:00:00Z",
+ last_used_at: null,
+ },
+ relationships: {
+ entity: { data: { type: "users", id: "user-1" } },
+ },
+ userEmail: "user@example.com",
+};
+
+// The app runs under React Compiler; only this suite compiles like it does.
+describe("API key modals under React Compiler", () => {
+ it("enables Create API Key once a name is typed", async () => {
+ // Given
+ const screen = await render(
+ ,
+ );
+ const submit = screen.getByRole("button", { name: "Create API Key" });
+ await expect.element(submit).toBeDisabled();
+
+ // When
+ await screen.getByPlaceholder("My API Key").fill("CI pipeline");
+
+ // Then
+ await expect.element(submit).toBeEnabled();
+ });
+
+ it("enables Save Changes once the name changes", async () => {
+ // Given
+ const screen = await render(
+ ,
+ );
+ const submit = screen.getByRole("button", { name: "Save Changes" });
+
+ // When
+ await screen.getByPlaceholder("My API Key").fill("Nightly export");
+
+ // Then
+ await expect.element(submit).toBeEnabled();
+ });
+});
diff --git a/ui/components/users/profile/api-keys-card-client.test.tsx b/ui/components/users/profile/api-keys-card-client.test.tsx
new file mode 100644
index 0000000000..118c39adea
--- /dev/null
+++ b/ui/components/users/profile/api-keys-card-client.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { useCloudUpgradeStore } from "@/store";
+import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
+
+import { ApiKeysCardClient } from "./api-keys-card-client";
+
+const { isCloudMock } = vi.hoisted(() => ({
+ isCloudMock: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ refresh: vi.fn() }),
+}));
+
+vi.mock("@/actions/api-keys/api-keys", () => ({
+ createApiKey: vi.fn(),
+ updateApiKey: vi.fn(),
+ revokeApiKey: vi.fn(),
+}));
+
+vi.mock("@/auth.config", () => ({ auth: vi.fn() }));
+
+vi.mock("@/lib/shared/env", () => ({
+ isCloud: isCloudMock,
+}));
+
+describe("ApiKeysCardClient", () => {
+ beforeEach(() => {
+ useCloudUpgradeStore.getState().closeCloudUpgrade();
+ });
+
+ it("offers Prowler Cloud instead of creating keys on Local Server", async () => {
+ // Given
+ isCloudMock.mockReturnValue(false);
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(screen.getByRole("button", { name: /Create API Key/ }));
+
+ // Then
+ expect(useCloudUpgradeStore.getState().activeFeature).toBe(
+ CLOUD_UPGRADE_FEATURE.API_KEYS,
+ );
+ expect(screen.queryByLabelText(/Expires in/)).not.toBeInTheDocument();
+ });
+
+ it("opens the create form in Prowler Cloud", async () => {
+ // Given
+ isCloudMock.mockReturnValue(true);
+ const user = userEvent.setup();
+ render();
+
+ // When
+ await user.click(screen.getByRole("button", { name: /Create API Key/ }));
+
+ // Then
+ expect(useCloudUpgradeStore.getState().activeFeature).toBeNull();
+ expect(await screen.findByLabelText(/Expires in/)).toBeVisible();
+ });
+});
diff --git a/ui/components/users/profile/api-keys-card-client.tsx b/ui/components/users/profile/api-keys-card-client.tsx
index 0adc0c405a..a87c32a83f 100644
--- a/ui/components/users/profile/api-keys-card-client.tsx
+++ b/ui/components/users/profile/api-keys-card-client.tsx
@@ -11,9 +11,13 @@ import {
CardHeader,
CardTitle,
} from "@/components/shadcn";
+import { Badge } from "@/components/shadcn/badge/badge";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { DataTable } from "@/components/shadcn/table";
+import { isCloud } from "@/lib/shared/env";
+import { useCloudUpgradeStore } from "@/store";
import { MetaDataProps } from "@/types";
+import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { ApiKeySuccessModal } from "./api-key-success-modal";
import { createApiKeyColumns } from "./api-keys/column-api-keys";
@@ -32,6 +36,11 @@ export const ApiKeysCardClient = ({
metadata,
}: ApiKeysCardClientProps) => {
const router = useRouter();
+ const openCloudUpgrade = useCloudUpgradeStore(
+ (state) => state.openCloudUpgrade,
+ );
+ // API key creation is a Prowler Cloud feature; Local Server gets the upsell.
+ const canCreateApiKeys = isCloud();
const [selectedApiKey, setSelectedApiKey] = useState(
null,
);
@@ -48,6 +57,15 @@ export const ApiKeysCardClient = ({
router.refresh();
};
+ const handleCreateClick = () => {
+ if (!canCreateApiKeys) {
+ openCloudUpgrade(CLOUD_UPGRADE_FEATURE.API_KEYS);
+ return;
+ }
+
+ setIsCreateModalOpen(true);
+ };
+
const handleRevokeSuccess = () => {
router.refresh();
};
@@ -73,7 +91,14 @@ export const ApiKeysCardClient = ({
-
API Keys
+
+ API Keys
+ {!canCreateApiKeys && (
+
+ Cloud
+
+ )}
+
Manage API keys for programmatic access.{" "}
@@ -82,11 +107,7 @@ export const ApiKeysCardClient = ({
-
diff --git a/ui/components/users/profile/create-api-key-modal.tsx b/ui/components/users/profile/create-api-key-modal.tsx
index e5dfa94fc3..cc569206f3 100644
--- a/ui/components/users/profile/create-api-key-modal.tsx
+++ b/ui/components/users/profile/create-api-key-modal.tsx
@@ -104,7 +104,7 @@ export const CreateApiKeyModal = ({
-
+
{
expect(titles).toEqual([
"Keep Every Provider Checked Automatically",
"Turn Findings into Alerts",
+ "Automate Prowler with API Keys",
"Add Your Entire AWS Organization",
"Add Your Entire Azure Management Group",
"Add Your Entire GCP Organization",
@@ -74,6 +75,7 @@ describe("cloud upgrade URLs", () => {
it.each([
[CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING, "advanced-scheduling"],
[CLOUD_UPGRADE_FEATURE.ALERTS, "alerts"],
+ [CLOUD_UPGRADE_FEATURE.API_KEYS, "api-keys"],
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS, "organization"],
[CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS, "azure-organization"],
[CLOUD_UPGRADE_FEATURE.CLI_IMPORT, "cli-import"],
diff --git a/ui/lib/cloud-upgrade.ts b/ui/lib/cloud-upgrade.ts
index d6fd06eb39..9d63bd4c8d 100644
--- a/ui/lib/cloud-upgrade.ts
+++ b/ui/lib/cloud-upgrade.ts
@@ -27,6 +27,7 @@ const LOCAL_SERVER_UTM_SOURCE = "prowler-local-server";
const CLOUD_UPGRADE_UTM_CONTENT = {
[CLOUD_UPGRADE_FEATURE.ADVANCED_SCHEDULING]: "advanced-scheduling",
[CLOUD_UPGRADE_FEATURE.ALERTS]: "alerts",
+ [CLOUD_UPGRADE_FEATURE.API_KEYS]: "api-keys",
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: "organization",
[CLOUD_UPGRADE_FEATURE.AZURE_ORGANIZATIONS]: "azure-organization",
[CLOUD_UPGRADE_FEATURE.CLI_IMPORT]: "cli-import",
@@ -64,6 +65,17 @@ export const CLOUD_UPGRADE_CONTENT = {
],
primaryCta: "Create Alerts in Prowler Cloud",
},
+ [CLOUD_UPGRADE_FEATURE.API_KEYS]: {
+ title: "Automate Prowler with API Keys",
+ description:
+ "Give scripts, pipelines, and integrations programmatic access to Prowler Cloud.",
+ benefits: [
+ "Create named keys with their own expiration",
+ "Revoke a key without touching your user account",
+ "See when each key was last used",
+ ],
+ primaryCta: "Create API Keys in Prowler Cloud",
+ },
[CLOUD_UPGRADE_FEATURE.AWS_ORGANIZATIONS]: {
title: "Add Your Entire AWS Organization",
description:
diff --git a/ui/types/cloud-upgrade.ts b/ui/types/cloud-upgrade.ts
index e7b860a8d5..925922b564 100644
--- a/ui/types/cloud-upgrade.ts
+++ b/ui/types/cloud-upgrade.ts
@@ -1,6 +1,7 @@
export const CLOUD_UPGRADE_FEATURE = {
ADVANCED_SCHEDULING: "advanced_scheduling",
ALERTS: "alerts",
+ API_KEYS: "api_keys",
AWS_ORGANIZATIONS: "aws_organizations",
AZURE_ORGANIZATIONS: "azure_organizations",
CLI_IMPORT: "cli_import",