merge master into grouped Jira UI

This commit is contained in:
Hugo P.Brito committed 2026-07-22 12:12:46 +01:00
commit 33ff2a2a44
316 files changed
+10112 -4023

No files matched your search

+20
View File
@@ -4,6 +4,26 @@ All notable changes to the **Prowler UI** are documented in this file.
<!-- changelog: release notes start -->
## [1.35.0] (Prowler v5.35.0)
### 🔄 Changed
- AWS Organizations onboarding now deploys the management account role and the member-account StackSet from a single CloudFormation stack, replacing the manual StackSet console step [(#11927)](https://github.com/prowler-cloud/prowler/pull/11927)
- Dynamic providers can now be renamed and deleted from the Providers table [(#11957)](https://github.com/prowler-cloud/prowler/pull/11957)
- Sidebar navigation with grouped sections, clearer active states, and a responsive mobile overlay [(#11994)](https://github.com/prowler-cloud/prowler/pull/11994)
- Core Prowler tools in Lighthouse use the `prowler_*` namespace while preserving legacy `prowler_app_*` compatibility [(#12017)](https://github.com/prowler-cloud/prowler/pull/12017)
### 🐞 Fixed
- The AWS S3 integration CloudFormation quick-create link now sets the bucket owner account ID, preventing a stack validation error when S3 integration is enabled [(#11927)](https://github.com/prowler-cloud/prowler/pull/11927)
- `Scan ID` filter on the Findings page now shows the active scan when opening findings from a scan's `View Findings` action [(#11997)](https://github.com/prowler-cloud/prowler/pull/11997)
### 🔐 Security
- `js-yaml` to 4.3.0, `@sentry/nextjs` to 10.65.0 with `import-in-the-middle` 3.3.1, and transitive `hono`, `dompurify`, `ws`, `vite`, `@babel/core` and `@opentelemetry/core` to patched versions, resolving 13 npm audit advisories (3 high, 9 moderate, 1 low) plus `hono` CVE-2026-59896, published on NVD but not yet in the npm audit feed [(#12029)](https://github.com/prowler-cloud/prowler/pull/12029)
---
## [1.34.0] (Prowler v5.34.0)
### 🚀 Added
+6 -3
View File
@@ -4,7 +4,9 @@ FROM node:24.13.0-alpine@sha256:cd6fb7efa6490f039f3471a189214d5f548c11df1ff9e5b1
LABEL maintainer="https://github.com/prowler-cloud"
# Patch Alpine OpenSSL runtime packages before all stages inherit the base image.
RUN apk upgrade --no-cache libcrypto3 libssl3 && corepack enable
# The build uses pnpm via corepack, so npm is unused — remove it (and npx) to drop
# the bundled-npm CVE surface (node-tar CVE-2026-59873) from every stage, incl. prod.
RUN apk upgrade --no-cache libcrypto3 libssl3 && corepack enable && rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx
# Install dependencies only when needed
FROM base AS deps
@@ -79,9 +81,10 @@ ENV HOSTNAME="0.0.0.0"
# Helm/K8s):
# - 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)
# - gated integrations (load only when *_ENABLED="true"; the value is then
# required or boot fails). Legacy names (NEXT_PUBLIC_*, POSTHOG_KEY/HOST)
# still activate without the flag:
# required or boot fails). Their legacy names (NEXT_PUBLIC_SENTRY_*,
# NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID, POSTHOG_KEY/HOST) still work:
# UI_SENTRY_ENABLED + UI_SENTRY_DSN (+ optional UI_SENTRY_ENVIRONMENT)
# UI_GOOGLE_TAG_MANAGER_ENABLED + UI_GOOGLE_TAG_MANAGER_ID
# UI_POSTHOG_ENABLED + UI_POSTHOG_KEY + UI_POSTHOG_HOST (no consumer yet)
@@ -235,7 +235,7 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => {
it("should attach adapter-produced triage DTOs to finding-level resource rows", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const input = {
data: [
{
@@ -291,7 +291,7 @@ describe("adaptFindingGroupResourcesResponse — malformed input", () => {
it("should leave triage editing disabled until a real capability is provided", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const input = {
data: [
{
@@ -1,3 +1,4 @@
import { isCloud } from "@/lib/shared/env";
import {
FINDING_TRIAGE_DISABLED_REASON,
type FindingTriageDisabledReason,
@@ -9,7 +10,7 @@ interface FindingTriageAdapterOptions {
}
export function getFindingTriageAdapterOptions(): FindingTriageAdapterOptions {
const isCloudEnvironment = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnvironment = isCloud();
return {
canEdit: isCloudEnvironment,
+2 -2
View File
@@ -55,7 +55,7 @@ describe("findings actions triage projection", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.stubGlobal("fetch", fetchMock);
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
getAuthHeadersMock.mockResolvedValue({ Authorization: "Bearer token" });
fetchMock.mockResolvedValue(new Response("", { status: 200 }));
handleApiResponseMock.mockResolvedValue(findingsResponse);
@@ -83,7 +83,7 @@ describe("findings actions triage projection", () => {
it("should attach domain triage DTOs to latest findings responses", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const result = await getLatestFindings({ page: 1, pageSize: 10 });
@@ -1,5 +1,5 @@
import { LucideIcon } from "lucide-react";
import {
LucideIcon,
Activity,
BarChart3,
Bot,
+2 -1
View File
@@ -13,6 +13,7 @@ import {
} from "@/lib";
import { appendSanitizedProviderTypeFilters } from "@/lib/provider-filters";
import { handleApiResponse } from "@/lib/server-actions-helper";
import { isCloud } from "@/lib/shared/env";
import { OrganizationResource } from "@/types/organizations";
export const getResources = async ({
@@ -287,7 +288,7 @@ export const getResourceDrawerData = async ({
pageSize?: number;
query?: string;
}) => {
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnv = isCloud();
const [resourceData, findingsResponse, organizationsResponse] =
await Promise.all([
+3 -3
View File
@@ -74,7 +74,7 @@ describe("role actions", () => {
it("includes manage_alerts when creating a role in Prowler Cloud", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
await addRole(makeRoleFormData());
@@ -85,7 +85,7 @@ describe("role actions", () => {
it("omits manage_alerts when creating a role outside Prowler Cloud", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
await addRole(makeRoleFormData());
@@ -98,7 +98,7 @@ describe("role actions", () => {
it("includes manage_alerts when updating a role in Prowler Cloud", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
await updateRole(makeRoleFormData(), "role-1");
+3 -2
View File
@@ -5,6 +5,7 @@ import { redirect } from "next/navigation";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
import { isCloud } from "@/lib/shared/env";
export const getRoles = async ({
page = 1,
@@ -108,7 +109,7 @@ export const addRole = async (formData: FormData) => {
};
// Conditionally include Prowler Cloud permissions.
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
if (isCloud()) {
payload.data.attributes.manage_billing =
formData.get("manage_billing") === "true";
payload.data.attributes.manage_alerts =
@@ -165,7 +166,7 @@ export const updateRole = async (formData: FormData, roleId: string) => {
};
// Conditionally include Prowler Cloud permissions.
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
if (isCloud()) {
payload.data.attributes.manage_billing =
formData.get("manage_billing") === "true";
payload.data.attributes.manage_alerts =
+7 -3
View File
@@ -1,6 +1,10 @@
import { AuthForm } from "@/components/auth/oss";
import { getAuthUrl, isGithubOAuthEnabled } from "@/lib/helper";
import { isGoogleOAuthEnabled } from "@/lib/helper";
import {
getAuthUrl,
isGithubOAuthEnabled,
isGoogleOAuthEnabled,
} from "@/lib/helper";
import { isCloud } from "@/lib/shared/env";
import { SearchParamsProps } from "@/types";
const SignUp = async ({
@@ -13,7 +17,7 @@ const SignUp = async ({
typeof resolvedSearchParams?.invitation_token === "string"
? resolvedSearchParams.invitation_token
: null;
const isCloudEnv = process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true";
const isCloudEnv = isCloud();
const GOOGLE_AUTH_URL = getAuthUrl("google");
const GITHUB_AUTH_URL = getAuthUrl("github");
@@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { LIGHTHOUSE_OVERVIEW_BANNER_HREF } from "../_lib/lighthouse-banner";
import { LighthouseOverviewBanner } from "./lighthouse-overview-banner";
describe("LighthouseOverviewBanner", () => {
@@ -5,6 +5,7 @@ import {
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { AttackSurface } from "./_components/attack-surface";
export const AttackSurfaceSSR = async ({ searchParams }: SSRComponentProps) => {
@@ -13,6 +13,7 @@ import {
filterProvidersByScope,
parseFilterIds,
} from "../../_lib/provider-scope";
import { RiskPlotClient } from "./risk-plot-client";
export async function RiskPlotSSR({
@@ -7,6 +7,7 @@ import {
import { SearchParamsProps } from "@/types";
import { pickFilterParams } from "../../_lib/filter-params";
import { RiskRadarViewClient } from "./risk-radar-view-client";
export async function RiskRadarViewSSR({
@@ -5,6 +5,7 @@ import {
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { ResourcesInventory } from "./_components/resources-inventory";
export const ResourcesInventorySSR = async ({
@@ -2,6 +2,7 @@ import { getFindingsBySeverity } from "@/actions/overview";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { RiskSeverityChart } from "./_components/risk-severity-chart";
export const RiskSeverityChartSSR = async ({
@@ -15,6 +15,7 @@ import {
} from "@/types/severities";
import { DEFAULT_TIME_RANGE } from "../_constants/time-range.constants";
import { type TimeRange, TimeRangeSelector } from "./time-range-selector";
interface FindingSeverityOverTimeProps {
@@ -3,6 +3,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { FindingSeverityOverTime } from "./_components/finding-severity-over-time";
import { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton";
import { DEFAULT_TIME_RANGE } from "./_constants/time-range.constants";
@@ -2,6 +2,7 @@ import { getThreatScore } from "@/actions/overview";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { ThreatScore } from "./_components/threat-score";
export const ThreatScoreSSR = async ({ searchParams }: SSRComponentProps) => {
@@ -5,6 +5,7 @@ import {
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { ComplianceWatchlist } from "./_components/compliance-watchlist";
export const ComplianceWatchlistSSR = async ({
@@ -2,6 +2,7 @@ import { getServicesOverview, ServiceOverview } from "@/actions/overview";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { ServiceWatchlist } from "./_components/service-watchlist";
export const ServiceWatchlistSSR = async ({
@@ -25,6 +25,7 @@ vi.mock("@/lib/server-actions-helper", () => ({
}));
import { ALERT_AGGREGATE_OPS, ALERT_TRIGGER_KINDS } from "../_types";
import {
createAlert,
deleteAlert,
@@ -15,12 +15,10 @@ import {
ALERT_TRIGGER_KINDS,
type AlertRule,
} from "@/app/(prowler)/alerts/_types";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DOCS_URLS } from "@/lib/external-urls";
import type { MetaDataProps } from "@/types";
import type { ScanEntity } from "@/types";
import type { MetaDataProps, ScanEntity } from "@/types";
import type { ProviderProps } from "@/types/providers";
import { toAlertPayload } from "../_lib/alert-adapter";
@@ -29,6 +27,7 @@ import type {
AlertFormSubmitResult,
AlertFormValues,
} from "../_types/alert-form";
import { AlertFormModal } from "./alert-form-modal";
import { AlertsEmptyState } from "./alerts-empty-state";
import { AlertsTable } from "./alerts-table";
@@ -28,8 +28,9 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
ToastAction,
useToast,
} from "@/components/shadcn";
import { ToastAction, useToast } from "@/components/shadcn";
import { useCloudUpgradeStore } from "@/store";
import type { ScanEntity } from "@/types";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
@@ -2,6 +2,7 @@ import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { ATTACK_PATHS_VIEW_STATES } from "../_lib/get-attack-paths-view-state";
import { AttackPathsStatusPanel } from "./attack-paths-status-panel";
describe("AttackPathsStatusPanel", () => {
@@ -34,6 +34,7 @@ import {
resolveHiddenFindingIds,
} from "../../_lib";
import { isFindingNode, layoutWithDagre } from "../../_lib/layout";
import { FindingNode } from "./nodes/finding-node";
import { InternetNode } from "./nodes/internet-node";
import { ResourceNode } from "./nodes/resource-node";
@@ -12,6 +12,7 @@ import type { GraphNode } from "@/types/attack-paths";
import { resolveNodeColors, resolveNodeVisual } from "../../../_lib";
import { FINDING_NODE_DIMENSIONS } from "../../../_lib/node-dimensions";
import { getNodeLabelDisplay } from "../../../_lib/node-label-lines";
import { HiddenHandles } from "./hidden-handles";
interface FindingNodeData {
@@ -5,6 +5,7 @@ import { type NodeProps } from "@xyflow/react";
import type { GraphNode } from "@/types/attack-paths";
import { resolveNodeColors } from "../../../_lib";
import { HiddenHandles } from "./hidden-handles";
interface InternetNodeData {
@@ -12,6 +12,7 @@ import type { GraphNode } from "@/types/attack-paths";
import { resolveNodeColors, resolveNodeVisual } from "../../../_lib";
import { RESOURCE_NODE_DIMENSIONS } from "../../../_lib/node-dimensions";
import { getNodeLabelDisplay } from "../../../_lib/node-label-lines";
import { HiddenHandles } from "./hidden-handles";
interface ResourceNodeData {
@@ -28,13 +28,13 @@ import {
import { StatusAlert } from "@/components/shared/status-alert";
import { useMountEffect } from "@/hooks/use-mount-effect";
import { isCloud } from "@/lib/shared/env";
import { attackPathsEmptyTour } from "@/lib/tours/attack-paths-empty.tour";
import {
attackPathsTour,
type AttackPathsTourTarget,
pickDemoQuery,
pickDemoScan,
} from "@/lib/tours/attack-paths.tour";
import { attackPathsEmptyTour } from "@/lib/tours/attack-paths-empty.tour";
import { advanceActiveTour, useDriverTour } from "@/lib/tours/use-driver-tour";
import type {
AttackPathQuery,
@@ -6,6 +6,7 @@ import { useCloudUpgradeStore } from "@/store";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import { COMPLIANCE_TAB } from "../_types";
import { CompliancePageTabs } from "./compliance-page-tabs";
import { getComplianceTab } from "./compliance-page-tabs.shared";
@@ -29,6 +29,7 @@ import {
parseCrossProviderFilters,
} from "../_lib/cross-provider-frameworks";
import { CROSS_PROVIDER_OVERVIEW_RESULT_STATUS } from "../_types";
import { CrossProviderErrorAlert } from "./cross-provider-error-alert";
import type {
CrossProviderAccountOption,
@@ -11,6 +11,7 @@ import {
CROSS_PROVIDER_OVERVIEW_RESULT_STATUS,
CROSS_PROVIDER_OVERVIEW_TYPE,
} from "../_types";
import { CrossProviderOverview } from "./cross-provider-overview";
vi.mock("../_actions/cross-provider", () => ({
@@ -15,6 +15,7 @@ import {
} from "../_lib/cross-provider-frameworks";
import type { CrossProviderFrameworkSummary } from "../_types";
import { CROSS_PROVIDER_OVERVIEW_RESULT_STATUS } from "../_types";
import { CrossProviderErrorAlert } from "./cross-provider-error-alert";
import type {
CrossProviderAccountOption,
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest";
import type { CheckProviderTypesMap, Requirement } from "@/types/compliance";
import type { CrossProviderRequirementExtras } from "../_types";
import { CrossProviderRequirementContent } from "./cross-provider-requirement-content";
const { clientAccordionContentMock } = vi.hoisted(() => ({
@@ -2,6 +2,7 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { ProviderBreakdownEntry } from "../_types";
import { ProviderCoverageCard } from "./provider-coverage-card";
vi.mock("@/components/icons/providers-badge/provider-type-icon", () => ({
@@ -1,4 +1,3 @@
import React from "react";
import { Suspense } from "react";
import { getRoles } from "@/actions/roles";
@@ -29,6 +29,7 @@ import {
import { Skeleton } from "@/components/shadcn/skeleton/skeleton";
import { ProviderIcon } from "../config/provider-icon";
import { ChatComposerPanel } from "./composer";
import { ChatEmptyState } from "./empty-state";
import { useLighthouseChatStore } from "./lighthouse-chat-store-provider";
@@ -42,6 +42,7 @@ import {
LIGHTHOUSE_CHAT_SURFACE,
LighthouseV2ChatView,
} from "../chat/lighthouse-v2-chat-view";
import { LighthousePanelChatSkeleton } from "./lighthouse-panel-chat-skeleton";
const PANEL_CHAT_STATUS = {
@@ -6,6 +6,7 @@ import { Suspense } from "react";
import { ConnectLLMProvider } from "@/components/lighthouse-v1/connect-llm-provider";
import { SelectBedrockAuthMethod } from "@/components/lighthouse-v1/select-bedrock-auth-method";
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
import { isCloud } from "@/lib/shared/env";
import type { LighthouseProvider } from "@/types/lighthouse-v1";
export const BEDROCK_AUTH_MODES = {
@@ -44,7 +45,7 @@ function ConnectContent() {
}
export default function ConnectLLMProviderPage() {
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
if (isCloud()) {
redirect(LIGHTHOUSE_ROUTE.SETTINGS);
}
@@ -12,8 +12,7 @@ import {
} from "@/actions/lighthouse-v1/lighthouse";
import { DeleteLLMProviderForm } from "@/components/lighthouse-v1/forms/delete-llm-provider-form";
import { WorkflowConnectLLM } from "@/components/lighthouse-v1/workflow";
import { Button } from "@/components/shadcn";
import { NavigationHeader } from "@/components/shadcn";
import { Button, NavigationHeader } from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
import type { LighthouseProvider } from "@/types/lighthouse-v1";
@@ -5,6 +5,7 @@ import { Suspense } from "react";
import { SelectModel } from "@/components/lighthouse-v1/select-model";
import { LIGHTHOUSE_ROUTE } from "@/lib/lighthouse-routes";
import { isCloud } from "@/lib/shared/env";
import type { LighthouseProvider } from "@/types/lighthouse-v1";
function SelectModelContent() {
@@ -27,7 +28,7 @@ function SelectModelContent() {
}
export default function SelectModelPage() {
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
if (isCloud()) {
redirect(LIGHTHOUSE_ROUTE.SETTINGS);
}
@@ -15,8 +15,8 @@ import {
FieldError,
Skeleton,
Textarea,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Modal } from "@/components/shadcn/modal";
import { fontMono } from "@/config/fonts";
@@ -4,8 +4,7 @@ import { useState } from "react";
import { toggleMuteRule } from "@/actions/mute-rules";
import { MuteRuleData } from "@/actions/mute-rules/types";
import { Switch } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Switch, useToast } from "@/components/shadcn";
interface MuteRuleEnabledToggleProps {
muteRule: MuteRuleData;
@@ -3,6 +3,7 @@ import { type ReactNode } from "react";
import { describe, expect, it, vi } from "vitest";
import { type MuteRuleTableData } from "./mute-rule-target-previews";
import { MuteRuleTargetsModal } from "./mute-rule-targets-modal";
vi.mock("@/components/shadcn/modal", () => ({
Modal: ({
@@ -21,8 +22,6 @@ vi.mock("@/components/shadcn/modal", () => ({
) : null,
}));
import { MuteRuleTargetsModal } from "./mute-rule-targets-modal";
const longMuteRule: MuteRuleTableData = {
type: "mute-rules",
id: "mute-rule-1",
@@ -17,8 +17,8 @@ import {
FieldLabel,
Input,
Textarea,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Modal } from "@/components/shadcn/modal";
import { DOCS_URLS } from "@/lib/external-urls";
@@ -10,8 +10,7 @@ import {
import { AccountsSelector } from "@/app/(prowler)/_overview/_components/accounts-selector";
import { BatchFiltersLayout } from "@/components/filters/batch-filters-layout";
import { ClearFiltersButton } from "@/components/filters/clear-filters-button";
import { Button, Card } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Card, useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Modal } from "@/components/shadcn/modal";
import { DataTable } from "@/components/shadcn/table";
+2 -2
View File
@@ -11,6 +11,8 @@ import {
import { getSchedules, getSchedulesPage } from "@/actions/schedules";
import { auth } from "@/auth.config";
import { PageReady } from "@/components/onboarding";
import { ScansPageShell } from "@/components/scans/scans-page-shell";
import { ScansProvidersEmptyState } from "@/components/scans/scans-providers-empty-state";
import {
appendPendingScheduleRowsToPage,
buildScheduledTabRows,
@@ -20,8 +22,6 @@ import {
getScanJobsUserFilters,
pickScheduleProviderFilters,
} from "@/components/scans/scans.utils";
import { ScansPageShell } from "@/components/scans/scans-page-shell";
import { ScansProvidersEmptyState } from "@/components/scans/scans-providers-empty-state";
import { SkeletonTableScans } from "@/components/scans/table";
import { ScanJobsTable } from "@/components/scans/table/scan-jobs-table";
import { ContentLayout } from "@/components/shadcn/content-layout";
@@ -1 +0,0 @@
AWS Organizations onboarding now deploys the management account role and the member-account StackSet from a single CloudFormation stack, replacing the manual StackSet console step
@@ -0,0 +1 @@
AWS Organizations setup modal now shows the "Enter a valid Organizational Unit or Root ID" hint in the error color, clarifying why the deployment button is disabled
@@ -1 +0,0 @@
Dynamic providers can now be renamed and deleted from the Providers table
@@ -0,0 +1 @@
Billing navigation is hidden when Cloud billing is disabled, including Enterprise deployments
@@ -1 +0,0 @@
`Scan ID` filter on the Findings page now shows the active scan when opening findings from a scan's `View Findings` action
@@ -0,0 +1 @@
Findings Severity Over Time chart Y-axis labels no longer overflow for large findings counts
@@ -0,0 +1 @@
OCI provider E2E tests no longer require or submit a region when adding or updating credentials
@@ -0,0 +1 @@
Contextual Cloud upgrade modal content remains stable throughout the closing animation
@@ -1 +0,0 @@
Core Prowler tools in Lighthouse use the `prowler_*` namespace while preserving legacy `prowler_app_*` compatibility
@@ -1 +0,0 @@
The AWS S3 integration CloudFormation quick-create link now sets the bucket owner account ID, preventing a stack validation error when S3 integration is enabled
@@ -0,0 +1 @@
Sidebar logo top spacing in the main app sidebar
@@ -1 +0,0 @@
Sidebar navigation with grouped sections, clearer active states, and a responsive mobile overlay
@@ -0,0 +1 @@
UI Sentry alerts now suppress non-actionable warnings and expected API/control-flow noise while preserving actionable runtime failures
@@ -0,0 +1 @@
Removed the unused `npm` CLI from the UI container image, eliminating the bundled `node-tar` `CVE-2026-59873` (and future bundled-npm CVEs); the image builds with `pnpm` via `corepack` and does not use `npm`
@@ -0,0 +1 @@
Bumped `vitest` and `@vitest/browser`, `@vitest/browser-playwright`, `@vitest/coverage-v8` from `4.1.8` to `4.1.10`, resolving the critical `@vitest/browser` Browser Mode file-access permission bypass (`GHSA-p63j-vcc4-9vmv`) flagged by `pnpm audit`; dev dependencies only, no runtime impact
+1 -1
View File
@@ -17,8 +17,8 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { getSafeCallbackPath } from "@/lib/auth-callback-url";
+1 -2
View File
@@ -15,8 +15,7 @@ import { AuthFooterLink } from "@/components/auth/oss/auth-footer-link";
import { AuthLayout } from "@/components/auth/oss/auth-layout";
import { PasswordRequirementsMessage } from "@/components/auth/oss/password-validator";
import { SocialButtons } from "@/components/auth/oss/social-buttons";
import { Button, Checkbox } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Checkbox, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
@@ -1,8 +1,15 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { SocialButtons } from "./social-buttons";
// Stub Iconify: the real <Icon> fetches icon data over the network and its
// retry timers can fire after the jsdom environment is torn down, crashing the
// worker with "window is not defined".
vi.mock("@iconify/react", () => ({
Icon: ({ icon }: { icon: string }) => <span aria-label={icon} />,
}));
describe("SocialButtons", () => {
it("renders icon-only provider links that keep their accessible names", () => {
render(
@@ -2,8 +2,7 @@
import { useRef, useState } from "react";
import { Button } from "@/components/shadcn";
import { Accordion, AccordionItemProps } from "@/components/shadcn";
import { Button, Accordion, AccordionItemProps } from "@/components/shadcn";
import { Card } from "@/components/shadcn/card/card";
export const ClientAccordionWrapper = ({
@@ -19,6 +19,7 @@ import {
import { ScanEntity } from "@/types/scans";
import { getComplianceIcon } from "../icons";
import { ComplianceDownloadContainer } from "./compliance-download-container";
interface ComplianceCardProps {
@@ -4,6 +4,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
import { CLOUD_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
import {
FINDING_TRIAGE_DISABLED_REASON,
FINDING_TRIAGE_STATUS,
type FindingTriageSummary,
} from "@/types/findings-triage";
import {
DataTableRowActions,
type FindingRowData,
} from "./data-table-row-actions";
import { FindingsSelectionContext } from "./findings-selection-context";
const { MuteFindingsModalMock, isGroupedJiraDispatchEnabledMock } = vi.hoisted(
() => ({
@@ -97,18 +108,6 @@ vi.mock("./finding-note-modal", () => ({
) : null,
}));
import {
FINDING_TRIAGE_DISABLED_REASON,
FINDING_TRIAGE_STATUS,
type FindingTriageSummary,
} from "@/types/findings-triage";
import {
DataTableRowActions,
type FindingRowData,
} from "./data-table-row-actions";
import { FindingsSelectionContext } from "./findings-selection-context";
function deferredPromise<T>() {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -22,8 +22,9 @@ import {
TableHead,
TableHeader,
TableRow,
SeverityBadge,
StatusFindingBadge,
} from "@/components/shadcn/table";
import { SeverityBadge, StatusFindingBadge } from "@/components/shadcn/table";
import { useFindingGroupResourceState } from "@/hooks/use-finding-group-resource-state";
import { cn, hasHistoricalFindingFilter } from "@/lib";
import { isGroupedJiraDispatchEnabled } from "@/lib/deployment";
@@ -37,6 +38,7 @@ import { FindingGroupRow } from "@/types";
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
import { FloatingMuteButton } from "../floating-mute-button";
import { getColumnFindingResources } from "./column-finding-resources";
import { FindingsSelectionContext } from "./findings-selection-context";
import { ImpactedResourcesCell } from "./impacted-resources-cell";
@@ -21,6 +21,7 @@ import { FindingGroupRow, MetaDataProps } from "@/types";
import { JIRA_DISPATCH_MODE, JIRA_DISPATCH_TARGET } from "@/types/integrations";
import { FloatingMuteButton } from "../floating-mute-button";
import { getColumnFindingGroups } from "./column-finding-groups";
import { canMuteFindingGroup } from "./finding-group-selection";
import { FindingsSelectionContext } from "./findings-selection-context";
@@ -71,8 +71,7 @@ import {
type QueryEditorLanguage,
} from "@/components/shared/query-code-editor";
import { ResourceMetadataPanel } from "@/components/shared/resource-metadata-panel";
import { getFailingForLabel } from "@/lib/date-utils";
import { formatDuration } from "@/lib/date-utils";
import { getFailingForLabel, formatDuration } from "@/lib/date-utils";
import { shouldRefreshAfterTriageUpdate } from "@/lib/finding-triage";
import { createJiraTargetSelection } from "@/lib/jira-dispatch-selection";
import { buildFindingAnalysisPrompt } from "@/lib/lighthouse/prompts";
@@ -91,6 +90,7 @@ import {
FindingTriageStatusCell,
} from "../finding-triage-cells";
import { DeltaValues, NotificationIndicator } from "../notification-indicator";
import { ResourceDetailSkeleton } from "./resource-detail-skeleton";
import type { CheckMeta } from "./use-resource-detail-drawer";
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { formatYAxisTick } from "./line-chart.utils";
describe("formatYAxisTick", () => {
describe("when findings counts are large", () => {
it("should compact six-digit values so Y-axis labels do not overflow", () => {
// Given
const tickValue = 150000;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("150K");
});
it("should compact million-scale values", () => {
// Given
const tickValue = 1200000;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("1.2M");
});
});
describe("when findings counts are small", () => {
it("should keep values below 1000 readable without compact notation", () => {
// Given
const tickValue = 999;
// When
const formattedValue = formatYAxisTick(tickValue);
// Then
expect(formattedValue).toBe("999");
});
});
});
+3
View File
@@ -17,6 +17,7 @@ import {
ChartTooltip,
} from "@/components/shadcn/chart/Chart";
import { formatYAxisTick } from "./line-chart.utils";
import { AlertPill } from "./shared/alert-pill";
import { ChartLegend } from "./shared/chart-legend";
import { CustomActiveDot, PointClickData } from "./shared/custom-active-dot";
@@ -222,6 +223,8 @@ export function LineChart({
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={formatYAxisTick}
width={56}
padding={{ top: 20 }}
tick={{
fill: "var(--color-text-neutral-secondary)",
+8
View File
@@ -0,0 +1,8 @@
const Y_AXIS_TICK_FORMATTER = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 1,
});
export function formatYAxisTick(value: number) {
return Y_AXIS_TICK_FORMATTER.format(value);
}
@@ -11,7 +11,7 @@ describe("ProwlerBrand", () => {
it("should render the Local Server lockups outside Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<ProwlerBrand />);
@@ -30,7 +30,7 @@ describe("ProwlerBrand", () => {
it("should render the Prowler Cloud lockups in Cloud", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
render(<ProwlerBrand />);
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { JiraIcon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const JiraIntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -15,15 +15,19 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { JiraIntegrationForm } from "./jira-integration-form";
interface JiraIntegrationsManagerProps {
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { AmazonS3Icon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const S3IntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -13,8 +13,7 @@ import {
ProviderTypeIcon,
} from "@/components/icons/providers-badge/provider-type-icon";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
@@ -26,6 +25,7 @@ import {
import { FormButtons } from "@/components/shadcn/form/form-buttons";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
import { getAWSCredentialsTemplateLinks } from "@/lib";
import { isCloud } from "@/lib/shared/env";
import type { AWSCredentialsRole } from "@/types";
import type { IntegrationProps } from "@/types/integrations";
import {
@@ -78,10 +78,9 @@ export const S3IntegrationForm = ({
const isEditingConfig = editMode === "configuration";
const isEditingCredentials = editMode === "credentials";
const defaultCredentialsType =
process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true"
? "aws-sdk-default"
: "access-secret-key";
const defaultCredentialsType = isCloud()
? "aws-sdk-default"
: "access-secret-key";
const form = useForm({
resolver: zodResolver(
@@ -15,8 +15,13 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
@@ -24,7 +29,6 @@ import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { ProviderProps } from "@/types/providers";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { S3IntegrationForm } from "./s3-integration-form";
interface S3IntegrationsManagerProps {
@@ -12,8 +12,13 @@ import { z } from "zod";
import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
import { AddIcon } from "@/components/icons";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Button,
Card,
CardContent,
CardHeader,
useToast,
} from "@/components/shadcn";
import { CodeSnippet } from "@/components/shadcn/code-snippet/code-snippet";
import { CustomServerInput } from "@/components/shadcn/custom";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
@@ -4,11 +4,9 @@ import { SettingsIcon } from "lucide-react";
import Link from "next/link";
import { AWSSecurityHubIcon } from "@/components/icons/services/IconServices";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
export const SecurityHubIntegrationCard = () => {
return (
<Card variant="base" padding="lg">
@@ -12,8 +12,7 @@ import {
ProviderTypeIcon,
} from "@/components/icons/providers-badge/provider-type-icon";
import { AWSRoleCredentialsForm } from "@/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form";
import { Checkbox, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Checkbox, Separator, useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import {
Form,
@@ -15,8 +15,14 @@ import {
IntegrationCardHeader,
IntegrationSkeleton,
} from "@/components/integrations/shared";
import { Badge, Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import {
Badge,
Button,
useToast,
Card,
CardContent,
CardHeader,
} from "@/components/shadcn";
import { Modal } from "@/components/shadcn/modal";
import { DataTablePagination } from "@/components/shadcn/table/data-table-pagination";
import { triggerTestConnectionWithDelay } from "@/lib/integrations/test-connection-helper";
@@ -24,7 +30,6 @@ import { MetaDataProps } from "@/types";
import { IntegrationProps } from "@/types/integrations";
import { ProviderProps } from "@/types/providers";
import { Card, CardContent, CardHeader } from "../../shadcn";
import { SecurityHubIntegrationForm } from "./security-hub-integration-form";
interface SecurityHubIntegrationsManagerProps {
@@ -3,11 +3,9 @@
import { ExternalLinkIcon, LucideIcon } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/shadcn";
import { Button, Card, CardContent, CardHeader } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { Card, CardContent, CardHeader } from "../../shadcn";
interface LinkCardProps {
icon: LucideIcon;
title: string;
@@ -7,8 +7,7 @@ import * as z from "zod";
import { revokeInvite } from "@/actions/invitations/invitation";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
@@ -5,7 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateInvite } from "@/actions/invitations/invitation";
import { useToast } from "@/components/shadcn";
import { useToast, Card, CardContent } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form, FormButtons } from "@/components/shadcn/form";
import {
@@ -17,8 +17,6 @@ import {
} from "@/components/shadcn/select/select";
import { editInviteFormSchema } from "@/types";
import { Card, CardContent } from "../../shadcn";
export const EditForm = ({
invitationId,
invitationEmail,
@@ -7,8 +7,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { sendInvite } from "@/actions/invitations/invitation";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import {
@@ -2,19 +2,18 @@
import { useControlledState } from "@react-stately/utils";
import { domAnimation, LazyMotion, m } from "framer-motion";
import type { ComponentProps } from "react";
import React from "react";
import { forwardRef, useMemo } from "react";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { cn } from "@/lib/utils";
export type VerticalStepProps = {
className?: string;
description?: React.ReactNode;
title?: React.ReactNode;
description?: ReactNode;
title?: ReactNode;
};
export interface VerticalStepsProps
extends React.HTMLAttributes<HTMLButtonElement> {
export interface VerticalStepsProps extends HTMLAttributes<HTMLButtonElement> {
/**
* An array of steps.
*
@@ -89,10 +88,7 @@ function CheckIcon(props: ComponentProps<"svg">) {
);
}
export const VerticalSteps = React.forwardRef<
HTMLButtonElement,
VerticalStepsProps
>(
export const VerticalSteps = forwardRef<HTMLButtonElement, VerticalStepsProps>(
(
{
color = "primary",
@@ -113,7 +109,7 @@ export const VerticalSteps = React.forwardRef<
onStepChange,
);
const colors = React.useMemo(() => {
const colors = useMemo(() => {
let userColor;
let fgColor;
@@ -65,7 +65,7 @@ describe("AppSidebarContent", () => {
it("shares the brand, Launch Scan action and Local Server Cloud affordances", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
vi.stubEnv("NEXT_PUBLIC_PROWLER_RELEASE_VERSION", "5.8.0");
const user = userEvent.setup();
@@ -91,7 +91,7 @@ describe("AppSidebarContent", () => {
it("keeps the existing Lighthouse chat sidebar in Cloud Chat mode", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
useAppSidebarMode.setState({ mode: APP_SIDEBAR_MODE.CHAT });
// When
@@ -109,7 +109,7 @@ describe("AppSidebarContent", () => {
it("opens the current scan modal instead of navigating from the scans route", async () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
pathnameValue.current = "/scans";
const user = userEvent.setup();
@@ -24,15 +24,20 @@ interface AppSidebarContentProps {
export function AppSidebarContent({ onSelect }: AppSidebarContentProps) {
const pathname = usePathname();
const { permissions } = useAuth();
const { apiDocsUrl } = useRuntimeConfig();
const { apiDocsUrl, cloudBillingEnabled } = useRuntimeConfig();
const mode = useAppSidebarMode((state) => state.mode);
const isCloudEnvironment = isCloud();
const sections = getNavigationConfig({ pathname, apiDocsUrl, permissions });
const sections = getNavigationConfig({
pathname,
apiDocsUrl,
cloudBillingEnabled,
permissions,
});
const showChat = isCloudEnvironment && mode === APP_SIDEBAR_MODE.CHAT;
return (
<div className="relative flex h-full min-h-0 w-full flex-col overflow-hidden">
<div className="shrink-0 px-5 pt-6 pb-7">
<div className="shrink-0 px-5 pt-8 pb-7">
<Link
href="/"
aria-label="Prowler home"
@@ -31,7 +31,7 @@ describe("getNavigationConfig", () => {
it("groups the Local Server navigation without losing available features", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const sections = getNavigationConfig({
@@ -71,7 +71,7 @@ describe("getNavigationConfig", () => {
it("models Local Server Cloud features as contextual upgrade actions", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const children = getConfigurationChildren();
@@ -108,7 +108,7 @@ describe("getNavigationConfig", () => {
it("uses Cloud destinations and current New badges", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const sections = getNavigationConfig({
@@ -148,7 +148,7 @@ describe("getNavigationConfig", () => {
it("keeps the Cloud Billing destination for users with billing permission", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const permissions = {
manage_billing: true,
} as RolePermissionAttributes;
@@ -157,6 +157,7 @@ describe("getNavigationConfig", () => {
const billing = getNavigationConfig({
pathname: "/billing",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
})
.flatMap((section) => section.items)
@@ -177,29 +178,40 @@ describe("getNavigationConfig", () => {
const permissions = {
manage_billing: false,
} as RolePermissionAttributes;
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// When
const cloudItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions,
}).flatMap((section) => section.items);
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
const enterpriseItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: false,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
vi.stubEnv("UI_CLOUD_ENABLED", "false");
const localItems = getNavigationConfig({
pathname: "/",
apiDocsUrl: null,
cloudBillingEnabled: true,
permissions: { ...permissions, manage_billing: true },
}).flatMap((section) => section.items);
// Then
expect(cloudItems.find((item) => item.label === "Billing")).toBeUndefined();
expect(
enterpriseItems.find((item) => item.label === "Billing"),
).toBeUndefined();
expect(localItems.find((item) => item.label === "Billing")).toBeUndefined();
});
it("keeps environment-specific API documentation destinations", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const localApiReference = getNavigationConfig({
@@ -209,7 +221,7 @@ describe("getNavigationConfig", () => {
.flatMap((section) => section.items)
.find((item) => item.label === "API Reference");
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const cloudApiReference = getNavigationConfig({
pathname: "/",
apiDocsUrl: "https://ignored.example/docs",
@@ -228,7 +240,7 @@ describe("getNavigationConfig", () => {
it("omits the Local Server API reference when no URL is configured", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const items = getNavigationConfig({
@@ -244,7 +256,7 @@ describe("getNavigationConfig", () => {
it("filters navigation by required permission after visible copy changes", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const sections = getNavigationConfig({
pathname: "/integrations",
apiDocsUrl: null,
@@ -282,7 +294,7 @@ describe("getNavigationConfig", () => {
it("keeps navigation when the required permission is granted", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
const permissions = {
manage_integrations: true,
} as RolePermissionAttributes;
@@ -308,7 +320,7 @@ describe("getNavigationConfig", () => {
it("matches complete route segments without stealing nested settings routes", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
const scanDetails = getNavigationConfig({
@@ -31,6 +31,7 @@ import {
interface NavigationConfigOptions {
pathname: string;
apiDocsUrl?: string | null;
cloudBillingEnabled?: boolean;
permissions?: RolePermissionAttributes;
}
@@ -106,6 +107,7 @@ export function filterNavigationByPermissions(
export function getNavigationConfig({
pathname,
apiDocsUrl = null,
cloudBillingEnabled = false,
permissions,
}: NavigationConfigOptions): NavigationSection[] {
const isCloudEnvironment = isCloud();
@@ -265,7 +267,7 @@ export function getNavigationConfig({
},
],
},
...(isCloudEnvironment
...(isCloudEnvironment && cloudBillingEnabled
? [
{
kind: NAVIGATION_ITEM_KIND.LINK,
@@ -67,7 +67,7 @@ describe("NavbarClient", () => {
navigationMocks.searchParams = new URLSearchParams();
window.localStorage.clear();
// Replay icon is Cloud-only.
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "true");
vi.stubEnv("UI_CLOUD_ENABLED", "true");
// Default: the current route's content has loaded, so the icon is enabled.
usePageReadyStore.setState({ readyPath: "/findings" });
useSidePanelStore.setState({
@@ -223,7 +223,7 @@ describe("NavbarClient", () => {
});
it("hides the replay icon entirely in self-hosted (OSS) deployments", () => {
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
render(
<NavbarClient
@@ -280,7 +280,7 @@ describe("NavbarClient", () => {
it("hides the Lighthouse AI side-panel trigger in self-hosted (OSS) deployments", () => {
// Given
vi.stubEnv("NEXT_PUBLIC_IS_CLOUD_ENV", "false");
vi.stubEnv("UI_CLOUD_ENABLED", "false");
// When
render(<NavbarClient title="Findings" />);
+1 -1
View File
@@ -34,8 +34,8 @@ import {
CardHeader,
CardTitle,
Combobox,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomLink } from "@/components/shadcn/custom/custom-link";
import { useMountEffect } from "@/hooks/use-mount-effect";
import type { LighthouseProvider } from "@/types/lighthouse-v1";
@@ -17,8 +17,8 @@ import {
CardContent,
CardHeader,
CardTitle,
useToast,
} from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { CustomTextarea } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
@@ -5,8 +5,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { createProviderGroup } from "@/actions/manage-groups";
import { Button, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
@@ -8,8 +8,7 @@ import * as z from "zod";
import { deleteProviderGroup } from "@/actions/manage-groups/manage-groups";
import { DeleteIcon } from "@/components/icons";
import { Button } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, useToast } from "@/components/shadcn";
import { Form } from "@/components/shadcn/form";
const formSchema = z.object({
@@ -7,8 +7,7 @@ import { Controller, useForm } from "react-hook-form";
import * as z from "zod";
import { updateProviderGroup } from "@/actions/manage-groups/manage-groups";
import { Button, Separator } from "@/components/shadcn";
import { useToast } from "@/components/shadcn";
import { Button, Separator, useToast } from "@/components/shadcn";
import { CustomInput } from "@/components/shadcn/custom";
import { Form } from "@/components/shadcn/form";
import { EnhancedMultiSelect } from "@/components/shadcn/select/enhanced-multi-select";
Loaded 100 of 316 files, more files were not shown because too many files have changed in this diff. Show more