Skip to content
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8ab575f
chore(ui): initialize Registry feature chain
Alan-TheGentleman Aug 20, 2026
d8016db
feat(ui): add Registry permission support
Alan-TheGentleman Aug 20, 2026
487f79e
feat(ui): add fresh Registry access authority
Alan-TheGentleman Aug 20, 2026
07ed513
feat(ui): add fail-closed Registry access boundaries
Alan-TheGentleman Aug 20, 2026
d2fb198
feat(ui): add non-secret Registry API adapters
Alan-TheGentleman Aug 21, 2026
feecafb
feat(ui): add complete Registry catalog model
Alan-TheGentleman Aug 21, 2026
5bb370e
feat(ui): add guarded Registry server integration
Alan-TheGentleman Aug 21, 2026
99a8101
feat(ui): add Registry artifact explorer
Alan-TheGentleman Aug 24, 2026
01dc698
feat(ui): harden Registry accessibility and acceptance
Alan-TheGentleman Aug 24, 2026
ff2da11
fix(ui): stabilize Registry acceptance flows
Alan-TheGentleman Aug 24, 2026
5fff469
fix(ui): revoke Registry access after denied actions
Alan-TheGentleman Aug 25, 2026
841d77c
refactor(ui): drop the client-side Registry eligibility lease
Alan-TheGentleman Aug 26, 2026
fe20e7f
feat(ui): redesign Registry as a marketplace
Alan-TheGentleman Aug 24, 2026
bddc52e
feat(ui): style Registry access dialog to match marketplace design
Alan-TheGentleman Aug 25, 2026
d5a8db9
fix(ui): keep Registry access dialog controls inside the dialog
Alan-TheGentleman Aug 25, 2026
dbe588b
fix(ui): surface credential failures inside the access dialog
Alan-TheGentleman Aug 25, 2026
2375726
test(ui): prove unknown Registry eligibility fails closed after the l…
Alan-TheGentleman Aug 25, 2026
2937123
refactor(ui): align Registry marketplace with repo skill conventions
Alan-TheGentleman Aug 25, 2026
1a97f25
test(ui): scope the marketplace-ready check to the main landmark
Alan-TheGentleman Aug 25, 2026
337913e
feat(ui): show provider logos across the Registry marketplace
Alan-TheGentleman Aug 26, 2026
b30f431
feat(ui): make Registry cards self-sufficient without a detail panel
Alan-TheGentleman Aug 26, 2026
0e65e08
feat(ui): validate Registry keys through the task watcher
Alan-TheGentleman Aug 26, 2026
1e8b669
fix(ui): read the flat owner attributes from the catalog
Alan-TheGentleman Aug 26, 2026
9bdeb87
feat(ui): polish Registry cards with provider cluster and owner logos
Alan-TheGentleman Aug 26, 2026
0607484
feat(ui): label unknown providers by name in the card cluster
Alan-TheGentleman Aug 26, 2026
579d7ea
feat(registry): support async artifact installation
Alan-TheGentleman Aug 27, 2026
67aab39
feat(registry): wire async artifact installation
Alan-TheGentleman Aug 27, 2026
1d264b3
feat(registry): add dynamic provider schema boundary
Alan-TheGentleman Aug 28, 2026
5aa4bad
feat(registry): add provider credential schema renderer
Alan-TheGentleman Aug 31, 2026
f151ebb
feat(registry): handle built-in artifacts
Alan-TheGentleman Aug 31, 2026
96e95bb
fix(registry): allow adding built-in artifacts
Alan-TheGentleman Aug 31, 2026
724267c
chore(ui): consolidate Registry stack with master
alejandrobailo Sep 8, 2026
a720edf
fix(ui): enforce Registry installation and task confirmation
alejandrobailo Sep 8, 2026
99c849f
style(ui): align Registry catalog with shared controls
alejandrobailo Sep 8, 2026
18ae73d
feat(ui): onboard installed Registry providers
alejandrobailo Sep 8, 2026
8ae9929
test(ui): verify consolidated Registry flow and document rollout
alejandrobailo Sep 8, 2026
727e5ac
fix(ui): resolve Registry access and operation review findings
alejandrobailo Sep 9, 2026
828a292
refactor(ui): remove unrelated shared animation changes
alejandrobailo Sep 9, 2026
eda1f5c
test(ui): isolate Registry acceptance and stabilize browser tests
alejandrobailo Sep 9, 2026
8afe91c
test(ui): prune redundant Registry coverage
alejandrobailo Sep 9, 2026
e86ac5b
docs(ui): update Registry validation and environment setup
alejandrobailo Sep 9, 2026
81d0c85
fix(ui): simplify Registry branding and access controls
alejandrobailo Sep 9, 2026
e8bea65
feat(ui): link artifact success toast to Providers
alejandrobailo Sep 9, 2026
7aa6412
fix(ui): accept long provider credential descriptions
alejandrobailo Sep 10, 2026
545df57
fix(ui): clarify provider credential form errors
alejandrobailo Sep 10, 2026
183c4f6
style(ui): align scan actions with tabs
alejandrobailo Sep 10, 2026
feeb9b5
feat(ui): add Registry provider tabs and align labels
alejandrobailo Sep 10, 2026
3dbf5ed
fix(ui): mask Registry provider API keys
alejandrobailo Sep 10, 2026
8cf1e5f
fix(ui): show provider creation progress
alejandrobailo Sep 10, 2026
a25dace
fix(ui): preserve word wrapping in toasts
alejandrobailo Sep 10, 2026
5b63684
fix(ui): clarify all providers tab label
alejandrobailo Sep 10, 2026
f8ffa99
fix(ui): show provider creation conflicts and allow retries
alejandrobailo Sep 10, 2026
d73c2b9
feat(ui): redesign Registry card metadata with artifact counts
alejandrobailo Sep 10, 2026
4037119
feat(ui): add provider logo tooltips to Registry cards
alejandrobailo Sep 10, 2026
7b64a56
fix(ui): hide downloads for built-in Registry artifacts
alejandrobailo Sep 10, 2026
7113147
fix(ui): show registry removal errors inside modal
alejandrobailo Sep 14, 2026
b181126
fix(ui): support Template provider credential fields
alejandrobailo Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/test-impact.yml
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,17 @@ modules:
e2e:
- ui/tests/home/**

- name: ui-registry
match:
- ui/actions/registry/**
- ui/app/**/registry/**
- ui/components/registry/**
- ui/lib/registry/**
- ui/tests/registry/**
tests: []
e2e:
- ui/tests/registry/**

- name: ui-shadcn
match:
- ui/components/shadcn/**
Expand Down
85 changes: 43 additions & 42 deletions .github/workflows/ui-e2e-tests-v2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ on:
- master
- "v5.*"
paths:
- '.github/workflows/ui-e2e-tests-v2.yml'
- '.github/test-impact.yml'
- 'ui/**'
- 'api/**' # API changes can affect UI E2E
- '!ui/CHANGELOG.md'
- '!api/CHANGELOG.md'
- ".github/workflows/ui-e2e-tests-v2.yml"
- ".github/test-impact.yml"
- "ui/**"
- "api/**" # API changes can affect UI E2E
- "!ui/CHANGELOG.md"
- "!api/CHANGELOG.md"

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
Expand All @@ -40,11 +40,11 @@ jobs:
(needs.impact-analysis.outputs.has-ui-e2e == 'true' || needs.impact-analysis.outputs.run-all == 'true')
runs-on: ubuntu-latest
env:
AUTH_SECRET: 'fallback-ci-secret-for-testing'
AUTH_SECRET: "fallback-ci-secret-for-testing"
AUTH_TRUST_HOST: true
NEXTAUTH_URL: 'http://localhost:3000'
AUTH_URL: 'http://localhost:3000'
UI_API_BASE_URL: 'http://localhost:8080/api/v1'
NEXTAUTH_URL: "http://localhost:3000"
AUTH_URL: "http://localhost:3000"
UI_API_BASE_URL: "http://localhost:8080/api/v1"
E2E_ADMIN_USER: ${{ secrets.E2E_ADMIN_USER }}
E2E_ADMIN_PASSWORD: ${{ secrets.E2E_ADMIN_PASSWORD }}
E2E_AWS_PROVIDER_ACCOUNT_ID: ${{ secrets.E2E_AWS_PROVIDER_ACCOUNT_ID }}
Expand All @@ -60,7 +60,7 @@ jobs:
E2E_M365_SECRET_ID: ${{ secrets.E2E_M365_SECRET_ID }}
E2E_M365_TENANT_ID: ${{ secrets.E2E_M365_TENANT_ID }}
E2E_M365_CERTIFICATE_CONTENT: ${{ secrets.E2E_M365_CERTIFICATE_CONTENT }}
E2E_KUBERNETES_CONTEXT: 'kind-kind'
E2E_KUBERNETES_CONTEXT: "kind-kind"
E2E_KUBERNETES_KUBECONFIG_PATH: /home/runner/.kube/config
E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY: ${{ secrets.E2E_GCP_BASE64_SERVICE_ACCOUNT_KEY }}
E2E_GCP_PROJECT_ID: ${{ secrets.E2E_GCP_PROJECT_ID }}
Expand Down Expand Up @@ -292,7 +292,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: 'ui/.nvmrc'
node-version-file: "ui/.nvmrc"

- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
Expand Down Expand Up @@ -340,55 +340,56 @@ jobs:
- name: Run E2E tests
working-directory: ./ui
run: |
REGISTRY_FIXTURE_SELECTED=false
if [[ "${RUN_ALL_TESTS}" == "true" ]] ||
[[ " ${E2E_TEST_PATHS} " == *" ui/tests/registry/"* ]]; then
REGISTRY_FIXTURE_SELECTED=true
fi

if [[ "${RUN_ALL_TESTS}" == "true" ]]; then
echo "Running ALL E2E tests..."
echo "Running all standard E2E tests..."
pnpm run test:e2e
else
echo "Running targeted E2E tests: ${E2E_TEST_PATHS}"
# Convert glob patterns to playwright test paths
# e.g., "ui/tests/providers/**" -> "tests/providers"
echo "Running targeted standard E2E tests: ${E2E_TEST_PATHS}"
TEST_PATHS="${E2E_TEST_PATHS}"
# Remove ui/ prefix and convert ** to empty (playwright handles recursion)
TEST_PATHS=$(echo "$TEST_PATHS" | sed 's|ui/||g' | sed 's|\*\*||g' | tr ' ' '\n' | sort -u)
# Drop auth setup helpers (not runnable test suites)
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^tests/setups/')
# Safety net: if bare "tests/" appears (from broad patterns like ui/tests/**),
# expand to specific subdirs to avoid Playwright discovering setup files
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vE '^tests/(setups|registry)/' || true)

if echo "$TEST_PATHS" | grep -qx 'tests/'; then
echo "Expanding bare 'tests/' to specific subdirs (excluding setups)..."
SPECIFIC_DIRS=""
for dir in tests/*/; do
[[ "$dir" == "tests/setups/" ]] && continue
[[ "$dir" == "tests/setups/" || "$dir" == "tests/registry/" ]] && continue
SPECIFIC_DIRS="${SPECIFIC_DIRS}${dir}"$'\n'
done
# Replace "tests/" with specific dirs, keep other paths
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/')
TEST_PATHS=$(echo "$TEST_PATHS" | grep -vx 'tests/' || true)
TEST_PATHS="${TEST_PATHS}"$'\n'"${SPECIFIC_DIRS}"
TEST_PATHS=$(echo "$TEST_PATHS" | grep -v '^$' | sort -u)
fi
if [[ -z "$TEST_PATHS" ]]; then
echo "No runnable E2E test paths after filtering setups"
exit 0
fi
# Filter out directories that don't contain any test files

VALID_PATHS=""
while IFS= read -r p; do
[[ -z "$p" ]] && continue
if find "$p" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then
VALID_PATHS="${VALID_PATHS}${p}"$'\n'
while IFS= read -r path; do
[[ -z "$path" ]] && continue
if find "$path" -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | head -1 | grep -q .; then
VALID_PATHS="${VALID_PATHS}${path}"$'\n'
else
echo "Skipping empty test directory: $p"
echo "Skipping empty test directory: $path"
fi
done <<< "$TEST_PATHS"
VALID_PATHS=$(echo "$VALID_PATHS" | grep -v '^$' || true)
if [[ -z "$VALID_PATHS" ]]; then
echo "No test files found in any resolved paths — skipping E2E"
exit 0

if [[ -n "$VALID_PATHS" ]]; then
TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ')
echo "Resolved standard test paths: $TEST_PATHS"
read -ra test_paths <<< "$TEST_PATHS"
pnpm exec playwright test "${test_paths[@]}"
else
echo "No standard E2E test paths selected."
fi
TEST_PATHS=$(echo "$VALID_PATHS" | tr '\n' ' ')
echo "Resolved test paths: $TEST_PATHS"
read -ra test_paths <<< "$TEST_PATHS"
pnpm exec playwright test "${test_paths[@]}"
fi

if [[ "$REGISTRY_FIXTURE_SELECTED" == "true" ]]; then
echo "Running self-contained Registry fixture acceptance."
pnpm run test:e2e:registry
fi

- name: Upload test reports
Expand Down
10 changes: 10 additions & 0 deletions docs/developer-guide/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ The former build-time variables map to the new runtime variables as follows:

`UI_CLOUD_ENABLED` is a plain runtime boolean flag that enables Prowler Cloud behavior when set to the exact string `"true"` and defaults to off; unlike the other renamed variables it has no legacy fallback, so `NEXT_PUBLIC_IS_CLOUD_ENV` is no longer read.

## Registry UI Rollout and Rollback

`UI_REGISTRY_ENABLED` is an optional runtime flag for Prowler Cloud and Private Cloud. Registry is eligible only when both `UI_CLOUD_ENABLED` and `UI_REGISTRY_ENABLED` are the exact string `"true"` and the current user has the backend-authorized `manage_registry` permission. Unset, `"false"`, or malformed values fail closed. The flag defaults to off and is not a replacement for backend authorization. Registry access is independent of billing; Private Cloud can use it with `CLOUD_BILLING_ENABLED=false`.

Roll out Registry only after the Registry backend dependency is deployed, intended roles have `manage_registry`, and acceptance with real credentials has exercised installation, provider account creation, credentials, connection, and scan launch. Deploy the UI with `UI_REGISTRY_ENABLED` unset or `"false"`; set it to `"true"` only in the prepared process environment, then restart or otherwise apply the environment update required by the platform. A Registry key must belong to the configured Registry environment; a production key does not authenticate against a development Registry.

The catalog displays all artifacts, including built-ins and packages containing only checks or compliance frameworks. Only external provider artifacts support Add. After confirmed installation, open Providers and select the option labeled Registry to configure an account. Creating accounts and running scans also require the corresponding provider and scan permissions. Removing an artifact keeps existing provider accounts, but future connections or scans can fail until the artifact is installed again.

To roll back, set `UI_REGISTRY_ENABLED=false` or remove it and apply the environment update. Proxy, page, and action checks deny on their next request. Navigation refreshes from server-authorized access when the page is requested again. Rollback does not delete Registry credentials, tenant artifact records, or provider accounts.

The build-time-only Sentry variables used for source-map upload — `SENTRY_ORG`, `SENTRY_PROJECT`, `SENTRY_AUTH_TOKEN`, and `SENTRY_RELEASE` — keep their names, as they are not part of Prowler Local Server's runtime configuration.

## Enabling Third-Party Integrations
Expand Down
2 changes: 2 additions & 0 deletions ui/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ ENV HOSTNAME="0.0.0.0"
# - required: UI_API_BASE_URL, AUTH_URL, AUTH_SECRET (missing ⇒ fail fast at boot)
# - optional: UI_API_DOCS_URL
# - optional: UI_CLOUD_ENABLED ("true" only in Prowler Cloud deployments)
# - optional: UI_REGISTRY_ENABLED ("true" only after the Registry dependency,
# Cloud role grant, and controlled acceptance are ready; unset/false hides Registry)
# - gated integrations (load only when *_ENABLED="true"; the value is then
# required or boot fails). Their legacy names (NEXT_PUBLIC_SENTRY_*,
# NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID, POSTHOG_KEY/HOST) still work:
Expand Down
28 changes: 26 additions & 2 deletions ui/actions/auth/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vi.mock("@/lib/sentry-breadcrumbs", () => ({

import { createNewUser, getUserByMe } from "./auth";

const userMeResponse = (roleAttributes: Record<string, boolean>) => ({
const userMeResponse = (roleAttributes: Record<string, unknown>) => ({
data: {
type: "users",
id: "019b1234-5678-7abc-9def-0123456789ab",
Expand All @@ -43,7 +43,7 @@ const userMeResponse = (roleAttributes: Record<string, boolean>) => ({
],
});

const mockUserMe = (roleAttributes: Record<string, boolean>) => {
const mockUserMe = (roleAttributes: Record<string, unknown>) => {
fetchMock.mockResolvedValue(
new Response(JSON.stringify(userMeResponse(roleAttributes)), {
status: 200,
Expand Down Expand Up @@ -178,6 +178,30 @@ describe("auth actions", () => {
expect(result.permissions.manage_users).toBe(true);
});

it("should carry an exact manage_registry permission into the session", async () => {
// Given
mockUserMe({ manage_registry: true });

// When
const result = await getUserByMe("access-token");

// Then
expect(result.permissions.manage_registry).toBe(true);
});

it.each([undefined, "true", "TRUE", 1])(
"should deny a malformed manage_registry value of %j",
async (manageRegistry) => {
// Given
mockUserMe({ manage_registry: manageRegistry });

// When
const result = await getUserByMe("access-token");

// Then
expect(result.permissions.manage_registry).toBe(false);
},
);
it("should forward an abort signal when loading the current user", async () => {
// Given
mockUserMe({ manage_users: true });
Expand Down
71 changes: 10 additions & 61 deletions ui/actions/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AuthError } from "next-auth";

import { signIn, signOut } from "@/auth.config";
import { apiBaseUrl } from "@/lib";
import { UserMeError } from "@/lib/auth-errors";
import { fetchCurrentUser } from "@/lib/auth/current-user";
import { addAuthEvent } from "@/lib/sentry-breadcrumbs";
import type { UtmParams } from "@/lib/utm";
import type { SignInFormData, SignUpFormData } from "@/types";
Expand Down Expand Up @@ -145,66 +145,15 @@ export const getUserByMe = async (
accessToken: string,
signal?: AbortSignal,
) => {
const url = new URL(`${apiBaseUrl}/users/me?include=roles`);

try {
const response = await fetch(url.toString(), {
method: "GET",
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${accessToken}`,
},
signal,
});

if (!response.ok) {
const errorMessage =
response.status === 401
? "Invalid or expired token"
: response.status === 403
? "Access denied"
: response.status === 404
? "User not found"
: "Unable to load user";
throw new UserMeError(errorMessage, response.status);
}

const parsedResponse = await response.json();

const userRole = parsedResponse.included?.find(
(item: any) => item.type === "roles",
);

const permissions = {
manage_users: userRole.attributes.manage_users || false,
manage_account: userRole.attributes.manage_account || false,
manage_providers: userRole.attributes.manage_providers || false,
manage_scans: userRole.attributes.manage_scans || false,
manage_ingestions: userRole.attributes.manage_ingestions || false,
manage_integrations: userRole.attributes.manage_integrations || false,
manage_billing: userRole.attributes.manage_billing || false,
manage_alerts: userRole.attributes.manage_alerts || false,
manage_lighthouse_ai_configuration:
userRole.attributes.manage_lighthouse_ai_configuration || false,
unlimited_visibility: userRole.attributes.unlimited_visibility || false,
};

return {
name: parsedResponse.data.attributes.name,
email: parsedResponse.data.attributes.email,
company: parsedResponse.data.attributes.company_name,
dateJoined: parsedResponse.data.attributes.date_joined,
permissions,
};
} catch (error: unknown) {
if (error instanceof UserMeError) throw error;

throw new UserMeError(
error instanceof Error
? error.message
: "Network error or server unreachable",
);
}
const currentUser = await fetchCurrentUser(accessToken, { signal });

return {
name: currentUser.name,
email: currentUser.email,
company: currentUser.company,
dateJoined: currentUser.dateJoined,
permissions: currentUser.permissions,
};
};

export async function logOut() {
Expand Down
Loading
Loading