mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-30 23:34:27 +00:00
Merge branch 'master' into PRWLR-6707-create-a-way-of-reporting-for-prowler-threatscore
This commit is contained in:
112 files changed
+7682
-405
No files matched your search
@@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Navigation link in Scans view to access Compliance Overview [(#8251)](https://github.com/prowler-cloud/prowler/pull/8251)
|
||||
- Status column for findings table in the Compliance Detail view [(#8244)](https://github.com/prowler-cloud/prowler/pull/8244)
|
||||
- Allow to restrict routes access based on user permissions [(#8287)](https://github.com/prowler-cloud/prowler/pull/8287)
|
||||
- Max character limit validation for Scan label [(#8319)](https://github.com/prowler-cloud/prowler/pull/8319)
|
||||
|
||||
### Security
|
||||
|
||||
@@ -21,6 +22,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
- Upgrade to Next.js 14.2.30 and lock TypeScript to 5.5.4 for ESLint compatibility [(#8189)](https://github.com/prowler-cloud/prowler/pull/8189)
|
||||
- Improved active step highlighting and updated step titles and descriptions in the Cloud Provider credentials update flow [(#8303)](https://github.com/prowler-cloud/prowler/pull/8303)
|
||||
- Refactored all existing links across the app to use new custom-link component for consistent styling [(#8341)](https://github.com/prowler-cloud/prowler/pull/8341)
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { RocketIcon } from "@/components/icons";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
@@ -27,9 +27,9 @@ export default function Error({
|
||||
We're sorry for the inconvenience. Please try again or contact
|
||||
support if the problem persists.
|
||||
</AlertDescription>
|
||||
<Link href="/" className="font-bold">
|
||||
<CustomLink href="/" target="_self" className="font-bold">
|
||||
Go to the homepage
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Button, Checkbox, Divider, Link, Tooltip } from "@nextui-org/react";
|
||||
import { Button, Checkbox, Divider, Tooltip } from "@nextui-org/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -15,6 +15,7 @@ import { NotificationIcon, ProwlerExtended } from "@/components/icons";
|
||||
import { ThemeSwitch } from "@/components/ThemeSwitch";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -301,13 +302,12 @@ export const AuthForm = ({
|
||||
onChange={(e) => field.onChange(e.target.checked)}
|
||||
>
|
||||
I agree with the
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://prowler.com/terms-of-service/"
|
||||
size="sm"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
</CustomLink>
|
||||
of Prowler
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
@@ -359,13 +359,9 @@ export const AuthForm = ({
|
||||
content={
|
||||
<div className="flex-inline text-small">
|
||||
Social Login with Google is not enabled.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-social-login/#google-oauth-configuration">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
}
|
||||
placement="right-start"
|
||||
@@ -392,13 +388,9 @@ export const AuthForm = ({
|
||||
content={
|
||||
<div className="flex-inline text-small">
|
||||
Social Login with Github is not enabled.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-social-login/#github-oauth-configuration">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
}
|
||||
placement="right-start"
|
||||
@@ -451,12 +443,16 @@ export const AuthForm = ({
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign up</Link>
|
||||
<CustomLink size="base" href="/sign-up" target="_self">
|
||||
Sign up
|
||||
</CustomLink>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log in</Link>
|
||||
<CustomLink size="base" href="/sign-in" target="_self">
|
||||
Log in
|
||||
</CustomLink>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { SeverityBadge } from "@/components/ui/table";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
ComplianceLink,
|
||||
} from "./shared-components";
|
||||
|
||||
export const AWSWellArchitectedCustomDetails = ({
|
||||
@@ -75,11 +75,9 @@ export const AWSWellArchitectedCustomDetails = ({
|
||||
|
||||
{requirement.implementation_guidance_url && (
|
||||
<ComplianceDetailSection title="Implementation Guidance">
|
||||
<ComplianceLink
|
||||
href={requirement.implementation_guidance_url as string}
|
||||
>
|
||||
<CustomLink href={requirement.implementation_guidance_url as string}>
|
||||
{requirement.implementation_guidance_url as string}
|
||||
</ComplianceLink>
|
||||
</CustomLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
</ComplianceDetailContainer>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
ComplianceLink,
|
||||
} from "./shared-components";
|
||||
|
||||
interface CISDetailsProps {
|
||||
@@ -121,7 +121,7 @@ export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
|
||||
{processReferences(requirement.references).map(
|
||||
(url: string, index: number) => (
|
||||
<div key={index}>
|
||||
<ComplianceLink href={url}>{url}</ComplianceLink>
|
||||
<CustomLink href={url}>{url}</CustomLink>
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
import {
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
ComplianceDetailContainer,
|
||||
ComplianceDetailSection,
|
||||
ComplianceDetailText,
|
||||
ComplianceLink,
|
||||
} from "./shared-components";
|
||||
|
||||
export const MITRECustomDetails = ({
|
||||
@@ -63,9 +63,9 @@ export const MITRECustomDetails = ({
|
||||
|
||||
{requirement.technique_url && (
|
||||
<ComplianceDetailSection title="MITRE ATT&CK Reference">
|
||||
<ComplianceLink href={requirement.technique_url as string}>
|
||||
<CustomLink href={requirement.technique_url as string}>
|
||||
{requirement.technique_url as string}
|
||||
</ComplianceLink>
|
||||
</CustomLink>
|
||||
</ComplianceDetailSection>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const ComplianceLink = ({
|
||||
href,
|
||||
children,
|
||||
}: {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="break-all text-sm text-blue-600 decoration-1 transition-colors hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const ComplianceDetailContainer = ({
|
||||
children,
|
||||
}: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Snippet } from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet";
|
||||
import { CustomSection } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { EntityInfoShort, InfoField } from "@/components/ui/entities";
|
||||
import { DateWithTime } from "@/components/ui/entities/date-with-time";
|
||||
import { SeverityBadge } from "@/components/ui/table/severity-badge";
|
||||
@@ -151,15 +151,14 @@ export const FindingDetail = ({
|
||||
{attributes.check_metadata.remediation.recommendation.text}
|
||||
</p>
|
||||
{attributes.check_metadata.remediation.recommendation.url && (
|
||||
<Link
|
||||
<CustomLink
|
||||
href={
|
||||
attributes.check_metadata.remediation.recommendation.url
|
||||
}
|
||||
target="_blank"
|
||||
className="text-sm text-blue-500 hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</CustomLink>
|
||||
)}
|
||||
</div>
|
||||
</InfoField>
|
||||
@@ -179,13 +178,12 @@ export const FindingDetail = ({
|
||||
{/* Additional Resources section */}
|
||||
{attributes.check_metadata.remediation.code.other && (
|
||||
<InfoField label="Additional Resources">
|
||||
<Link
|
||||
<CustomLink
|
||||
href={attributes.check_metadata.remediation.code.other}
|
||||
target="_blank"
|
||||
className="text-sm text-blue-500 hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
View documentation
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</InfoField>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +8,7 @@ import { createSamlConfig, updateSamlConfig } from "@/actions/integrations";
|
||||
import { AddIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton, CustomServerInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { SnippetChip } from "@/components/ui/entities";
|
||||
import { FormButtons } from "@/components/ui/form";
|
||||
import { apiBaseUrl } from "@/lib";
|
||||
@@ -239,9 +239,9 @@ export const SamlConfigForm = ({
|
||||
assign the user's role. If the role does not exist, one will
|
||||
be created with minimal permissions. You can assign permissions to
|
||||
roles on the{" "}
|
||||
<Link href="/roles">
|
||||
<span className="underline">Roles</span>
|
||||
</Link>{" "}
|
||||
<CustomLink href="/roles" target="_self">
|
||||
<span>Roles</span>
|
||||
</CustomLink>{" "}
|
||||
page.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { Card, CardBody, CardHeader } from "@nextui-org/react";
|
||||
import { Link } from "@nextui-org/react";
|
||||
import { CheckIcon, Trash2Icon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { deleteSamlConfig } from "@/actions/integrations";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomAlertModal, CustomButton } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
|
||||
import { SamlConfigForm } from "./forms";
|
||||
|
||||
@@ -73,14 +73,9 @@ export const SamlIntegrationCard = ({ samlConfig }: { samlConfig?: any }) => {
|
||||
) : (
|
||||
<>
|
||||
Configure SAML Single Sign-On for secure authentication.{" "}
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-sso"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs font-medium text-primary"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/prowler-app-sso">
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { MemoizedMarkdown } from "@/components/lighthouse/memoized-markdown";
|
||||
import { CustomButton, CustomTextarea } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Form } from "@/components/ui/form";
|
||||
|
||||
interface SuggestedAction {
|
||||
@@ -138,12 +138,14 @@ export const Chat = ({ hasConfig, isActive }: ChatProps) => {
|
||||
? "Please configure your OpenAI API key to use Lighthouse AI."
|
||||
: "OpenAI API key is invalid. Please update your key to use Lighthouse AI."}
|
||||
</p>
|
||||
<Link
|
||||
<CustomLink
|
||||
href="/lighthouse/config"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
||||
target="_self"
|
||||
size="sm"
|
||||
>
|
||||
Configure API Key
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Textarea } from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
import { DeleteIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { FormButtons } from "@/components/ui/form";
|
||||
import { fontMono } from "@/config/fonts";
|
||||
import { convertToYaml, parseYamlValidation } from "@/lib/yaml";
|
||||
@@ -178,13 +178,9 @@ export const MutedFindingsConfigForm = ({
|
||||
</li>
|
||||
<li>
|
||||
Learn more about configuring the Mutelist{" "}
|
||||
<Link
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/mutelist/"
|
||||
target="_blank"
|
||||
className="text-primary-600 hover:underline"
|
||||
>
|
||||
<CustomLink href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/mutelist/">
|
||||
here
|
||||
</Link>
|
||||
</CustomLink>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { Checkbox } from "@nextui-org/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -18,6 +17,7 @@ import { getTask } from "@/actions/task/tasks";
|
||||
import { CheckIcon, RocketIcon } from "@/components/icons";
|
||||
import { useToast } from "@/components/ui";
|
||||
import { CustomButton } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { checkTaskStatus } from "@/lib/helper";
|
||||
import { ProviderType } from "@/types";
|
||||
@@ -295,8 +295,9 @@ export const TestConnectionForm = ({
|
||||
|
||||
<div className="flex w-full justify-end sm:space-x-6">
|
||||
{apiErrorMessage ? (
|
||||
<Link
|
||||
<CustomLink
|
||||
href="/providers"
|
||||
target="_self"
|
||||
className="mr-3 flex w-fit items-center justify-center space-x-2 rounded-lg border border-solid border-gray-200 px-4 py-2 hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<Icon
|
||||
@@ -304,7 +305,7 @@ export const TestConnectionForm = ({
|
||||
className="h-5 w-5 text-gray-600 dark:text-gray-400"
|
||||
/>
|
||||
<span>Back to providers</span>
|
||||
</Link>
|
||||
</CustomLink>
|
||||
) : connectionStatus?.error ? (
|
||||
<CustomButton
|
||||
onPress={isUpdated ? () => router.back() : onResetCredentials}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { Control } from "react-hook-form";
|
||||
|
||||
import { InfoIcon } from "@/components/icons";
|
||||
import { CustomInput } from "@/components/ui/custom";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { M365Credentials } from "@/types";
|
||||
|
||||
export const M365CredentialsForm = ({
|
||||
@@ -57,14 +57,12 @@ export const M365CredentialsForm = ({
|
||||
{" "}
|
||||
User and password authentication is being deprecated due to
|
||||
Microsoft's on-going MFA enforcement across all tenants (see{" "}
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://azure.microsoft.com/en-us/blog/announcing-mandatory-multi-factor-authentication-for-azure-sign-in/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
Microsoft docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
).
|
||||
</p>
|
||||
|
||||
@@ -76,14 +74,12 @@ export const M365CredentialsForm = ({
|
||||
</div>
|
||||
<p className="text-sm text-default-500">
|
||||
Due to that change, you must only{" "}
|
||||
<Link
|
||||
<CustomLink
|
||||
href="https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/microsoft365/getting-started-m365/#step-3-configure-your-m365-account"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline"
|
||||
size="sm"
|
||||
>
|
||||
use application authentication
|
||||
</Link>{" "}
|
||||
</CustomLink>{" "}
|
||||
to maintain all Prowler M365 scan capabilities.
|
||||
</p>
|
||||
<CustomInput
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
"use client";
|
||||
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import { getProviderName } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderLogo } from "@/components/ui/entities/get-provider-logo";
|
||||
import { getProviderHelpText } from "@/lib";
|
||||
@@ -24,13 +25,12 @@ export const ProviderTitleDocs = ({
|
||||
<p className="text-sm text-default-500">
|
||||
{getProviderHelpText(providerType as string).text}
|
||||
</p>
|
||||
<Link
|
||||
<CustomLink
|
||||
href={getProviderHelpText(providerType as string).link}
|
||||
target="_blank"
|
||||
className="text-sm font-medium text-primary"
|
||||
size="sm"
|
||||
>
|
||||
Read the docs
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,9 +29,13 @@ export const LaunchScanWorkflow = ({
|
||||
const formSchema = z.object({
|
||||
...onDemandScanFormSchema().shape,
|
||||
scanName: z
|
||||
.string()
|
||||
.min(3, "Must have at least 3 characters")
|
||||
.or(z.literal(""))
|
||||
.union([
|
||||
z
|
||||
.string()
|
||||
.min(3, "Must be at least 3 characters")
|
||||
.max(32, "Must not exceed 32 characters"),
|
||||
z.literal(""),
|
||||
])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -101,7 +105,7 @@ export const LaunchScanWorkflow = ({
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -50 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="min-w-48 self-end"
|
||||
className="h-[3.4rem] min-w-[15.2rem] self-end"
|
||||
>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "@/lib";
|
||||
|
||||
interface CustomLinkProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
href: string;
|
||||
target?: "_self" | "_blank" | string;
|
||||
ariaLabel?: string;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
scroll?: boolean;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
export const CustomLink = React.forwardRef<HTMLAnchorElement, CustomLinkProps>(
|
||||
(
|
||||
{
|
||||
href,
|
||||
target = "_blank",
|
||||
ariaLabel,
|
||||
className,
|
||||
children,
|
||||
scroll = true,
|
||||
size = "xs",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<Link
|
||||
ref={ref}
|
||||
href={href}
|
||||
scroll={scroll}
|
||||
className={cn(
|
||||
`text-${size} break-all font-medium text-primary decoration-1 hover:underline`,
|
||||
className,
|
||||
)}
|
||||
aria-label={ariaLabel}
|
||||
target={target}
|
||||
rel="noopener noreferrer"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CustomLink.displayName = "CustomLink";
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { LogOut, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { logOut } from "@/actions/auth";
|
||||
@@ -10,6 +9,8 @@ import {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/ui/avatar/avatar";
|
||||
import { Button } from "@/components/ui/button/button";
|
||||
import { CustomLink } from "@/components/ui/custom/custom-link";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -26,8 +27,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip/tooltip";
|
||||
|
||||
import { Button } from "../button/button";
|
||||
|
||||
export const UserNav = () => {
|
||||
const { data: session } = useSession();
|
||||
|
||||
@@ -80,10 +79,14 @@ export const UserNav = () => {
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem className="hover:cursor-pointer" asChild>
|
||||
<Link href="/profile" className="flex items-center">
|
||||
<CustomLink
|
||||
href="/profile"
|
||||
className="flex items-center font-normal text-prowler-black dark:text-prowler-white"
|
||||
target="_self"
|
||||
>
|
||||
<User className="text-muted-foreground mr-3 h-4 w-4" />
|
||||
Account
|
||||
</Link>
|
||||
</CustomLink>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
@@ -127,8 +127,8 @@ You operate in an agent loop, iterating through these steps:
|
||||
- Fetches information related to:
|
||||
- All findings data across providers. Supports filtering by severity, status, etc.
|
||||
- Unique metadata values from findings
|
||||
- Remediation for checks
|
||||
- Check IDs supported by different provider types
|
||||
- Available checks for a specific provider (aws, gcp, azure, kubernetes, etc)
|
||||
- Details of a specific check including details about severity, risk, remediation, compliances that are associated with the check, etc
|
||||
|
||||
### roles_agent
|
||||
|
||||
|
||||
+5
-1
@@ -15,7 +15,11 @@ const cspHeader = `
|
||||
|
||||
module.exports = {
|
||||
poweredByHeader: false,
|
||||
output: "standalone",
|
||||
// Use standalone only in production deployments, not for CI/testing
|
||||
...(process.env.NODE_ENV === "production" &&
|
||||
!process.env.CI && {
|
||||
output: "standalone",
|
||||
}),
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
Generated
+3
-3
@@ -10075,9 +10075,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
|
||||
+13
-3
@@ -8,6 +8,9 @@ export default defineConfig({
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [["list"]],
|
||||
outputDir: "/tmp/playwright-tests",
|
||||
expect: {
|
||||
timeout: 20000,
|
||||
},
|
||||
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
@@ -24,11 +27,18 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
webServer: {
|
||||
command: process.env.CI
|
||||
? "npm run build && npm run start:standalone"
|
||||
: "npm run dev",
|
||||
command: process.env.CI ? "npm run start" : "npm run dev",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 120 * 1000,
|
||||
env: {
|
||||
NEXT_PUBLIC_API_BASE_URL:
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8080/api/v1",
|
||||
AUTH_SECRET: process.env.AUTH_SECRET || "fallback-ci-secret-for-testing",
|
||||
AUTH_TRUST_HOST: process.env.AUTH_TRUST_HOST || "true",
|
||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:3000",
|
||||
E2E_USER: process.env.E2E_USER || "e2e@prowler.com",
|
||||
E2E_PASSWORD: process.env.E2E_PASSWORD || "Thisisapassword123@",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import {
|
||||
goToLogin,
|
||||
goToSignUp,
|
||||
fillLoginForm,
|
||||
submitLoginForm,
|
||||
login,
|
||||
verifySuccessfulLogin,
|
||||
verifyLoginError,
|
||||
verifyLoginFormElements,
|
||||
verifyDashboardRoute,
|
||||
toggleSamlMode,
|
||||
verifySamlModeActive,
|
||||
goBackFromSaml,
|
||||
verifyNormalModeActive,
|
||||
logout,
|
||||
verifyLogoutSuccess,
|
||||
waitForPageLoad,
|
||||
TEST_CREDENTIALS,
|
||||
ERROR_MESSAGES,
|
||||
URLS,
|
||||
verifyLoadingState,
|
||||
} from "./helpers";
|
||||
|
||||
test.describe("Login Flow", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await goToLogin(page);
|
||||
});
|
||||
|
||||
test("should display login form elements", async ({ page }) => {
|
||||
await verifyLoginFormElements(page);
|
||||
});
|
||||
|
||||
test("should successfully login with valid credentials", async ({ page }) => {
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
await verifyDashboardRoute(page);
|
||||
});
|
||||
|
||||
test("should show error message with invalid credentials", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Attempt login with invalid credentials
|
||||
await login(page, TEST_CREDENTIALS.INVALID);
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS);
|
||||
});
|
||||
|
||||
test("should handle empty form submission", async ({ page }) => {
|
||||
// Submit empty form
|
||||
await submitLoginForm(page);
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS);
|
||||
// Verify we're still on login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
|
||||
/*
|
||||
TODO: This test is failing, need UI work before.
|
||||
test("should validate email format", async ({ page }) => {
|
||||
// Attempt login with invalid email format
|
||||
await login(page, TEST_CREDENTIALS.INVALID_EMAIL_FORMAT);
|
||||
// Verify error message (application shows generic error for invalid email format too)
|
||||
await verifyLoginError(page, ERROR_MESSAGES.INVALID_CREDENTIALS);
|
||||
// Verify we're still on login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
*/
|
||||
|
||||
test("should toggle SAML SSO mode", async ({ page }) => {
|
||||
// Toggle to SAML mode
|
||||
await toggleSamlMode(page);
|
||||
await verifySamlModeActive(page);
|
||||
// Toggle back to normal mode
|
||||
await goBackFromSaml(page);
|
||||
await verifyNormalModeActive(page);
|
||||
});
|
||||
|
||||
test("should show loading state during form submission", async ({ page }) => {
|
||||
// Fill valid credentials
|
||||
await fillLoginForm(
|
||||
page,
|
||||
TEST_CREDENTIALS.VALID.email,
|
||||
TEST_CREDENTIALS.VALID.password,
|
||||
);
|
||||
// Submit form and verify loading state
|
||||
await submitLoginForm(page);
|
||||
// Verify loading state
|
||||
await verifyLoadingState(page);
|
||||
});
|
||||
|
||||
test("should handle SAML authentication flow", async ({ page }) => {
|
||||
// Enter email for SAML
|
||||
const samlEmail = "user@saml-domain.com";
|
||||
// Toggle to SAML mode
|
||||
await toggleSamlMode(page);
|
||||
// Fill email (password should be hidden)
|
||||
await page.getByLabel("Email").fill(samlEmail);
|
||||
// Submit should trigger SAML redirect (we can't test the actual SAML flow in E2E)
|
||||
// but we can verify the form submission
|
||||
await submitLoginForm(page);
|
||||
|
||||
// Note: In a real scenario, this would redirect to IdP
|
||||
// For testing, we just verify the form was submitted
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Session Persistence", () => {
|
||||
test("should maintain session after browser refresh", async ({ page }) => {
|
||||
// Login first
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
// Refresh the page
|
||||
await page.reload();
|
||||
await waitForPageLoad(page);
|
||||
// Verify session is maintained
|
||||
await expect(page).toHaveURL(URLS.DASHBOARD);
|
||||
await verifyDashboardRoute(page);
|
||||
// Verify user is not redirected back to login
|
||||
await expect(page).not.toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
|
||||
test("should redirect to login when accessing protected route without session", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Try to access protected route without login
|
||||
await page.goto(URLS.DASHBOARD);
|
||||
// Should be redirected to login page
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should logout successfully", async ({ page }) => {
|
||||
// Login first
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
// Logout
|
||||
await logout(page);
|
||||
await verifyLogoutSuccess(page);
|
||||
// Verify cannot access protected route after logout
|
||||
await page.goto(URLS.DASHBOARD);
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
|
||||
test("should handle session timeout gracefully", async ({ page }) => {
|
||||
// Login first
|
||||
await goToLogin(page);
|
||||
await login(page, TEST_CREDENTIALS.VALID);
|
||||
await verifySuccessfulLogin(page);
|
||||
// Simulate session timeout by clearing cookies
|
||||
await page.context().clearCookies();
|
||||
// Try to navigate to a protected route
|
||||
await page.goto(URLS.PROFILE);
|
||||
// Should be redirected to login
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Navigation", () => {
|
||||
test("should navigate to sign up page", async ({ page }) => {
|
||||
await goToLogin(page);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await expect(page).toHaveURL(URLS.SIGNUP);
|
||||
});
|
||||
|
||||
test("should navigate from sign up back to sign in", async ({ page }) => {
|
||||
await goToSignUp(page);
|
||||
await page.getByRole("link", { name: "Log in" }).click();
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("should handle browser back button correctly", async ({ page }) => {
|
||||
await goToLogin(page);
|
||||
await page.getByRole("link", { name: "Sign up" }).click();
|
||||
await expect(page).toHaveURL(URLS.SIGNUP);
|
||||
await page.goBack();
|
||||
await expect(page).toHaveURL(URLS.LOGIN);
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Accessibility", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await goToLogin(page);
|
||||
});
|
||||
|
||||
test("should be navigable with keyboard", async ({ page }) => {
|
||||
// Tab through form elements
|
||||
await page.keyboard.press("Tab"); // Toggle theme
|
||||
await page.keyboard.press("Tab"); // Email field
|
||||
await expect(page.getByLabel("Email")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Tab"); // Password field
|
||||
await expect(page.getByLabel("Password")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("Tab"); // Show password button
|
||||
await page.keyboard.press("Tab"); // Login button
|
||||
|
||||
if (process.env.NEXT_PUBLIC_IS_CLOUD_ENV === "true") {
|
||||
await page.keyboard.press("Tab"); // Forgot password
|
||||
}
|
||||
|
||||
await expect(page.getByRole("button", { name: "Log in" })).toBeFocused();
|
||||
});
|
||||
|
||||
test("should have proper ARIA labels", async ({ page }) => {
|
||||
await expect(page.getByRole("textbox", { name: "Email" })).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "Password" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Log in" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("has title", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test("get started link", async ({ page }) => {
|
||||
await page.goto("https://playwright.dev/");
|
||||
await page.getByRole("link", { name: "Get started" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Installation" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Page, expect } from "@playwright/test";
|
||||
|
||||
export const ERROR_MESSAGES = {
|
||||
INVALID_CREDENTIALS: "Invalid email or password",
|
||||
} as const;
|
||||
|
||||
export const URLS = {
|
||||
LOGIN: "/sign-in",
|
||||
SIGNUP: "/sign-up",
|
||||
DASHBOARD: "/",
|
||||
PROFILE: "/profile",
|
||||
} as const;
|
||||
|
||||
export const TEST_CREDENTIALS = {
|
||||
VALID: {
|
||||
email: process.env.E2E_USER || "e2e@prowler.com",
|
||||
password: process.env.E2E_PASSWORD || "Thisisapassword123@",
|
||||
},
|
||||
INVALID: {
|
||||
email: "invalid@example.com",
|
||||
password: "wrongPassword",
|
||||
},
|
||||
INVALID_EMAIL_FORMAT: {
|
||||
email: "invalid-email",
|
||||
password: "somepassword",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export async function goToLogin(page: Page) {
|
||||
await page.goto("/sign-in");
|
||||
}
|
||||
|
||||
export async function goToSignUp(page: Page) {
|
||||
await page.goto("/sign-up");
|
||||
}
|
||||
|
||||
export async function fillLoginForm(
|
||||
page: Page,
|
||||
email: string,
|
||||
password: string,
|
||||
) {
|
||||
await page.getByLabel("Email").fill(email);
|
||||
await page.getByLabel("Password").fill(password);
|
||||
}
|
||||
|
||||
export async function submitLoginForm(page: Page) {
|
||||
await page.getByRole("button", { name: "Log in" }).click();
|
||||
}
|
||||
|
||||
export async function login(
|
||||
page: Page,
|
||||
credentials: { email: string; password: string } = TEST_CREDENTIALS.VALID,
|
||||
) {
|
||||
await fillLoginForm(page, credentials.email, credentials.password);
|
||||
await submitLoginForm(page);
|
||||
}
|
||||
|
||||
export async function verifySuccessfulLogin(page: Page) {
|
||||
await expect(page).toHaveURL("/");
|
||||
await expect(page.locator("main")).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel("Breadcrumbs")
|
||||
.getByRole("heading", { name: "Overview", exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
export async function verifyLoginError(
|
||||
page: Page,
|
||||
errorMessage = "Invalid email or password",
|
||||
) {
|
||||
await expect(page.getByText(errorMessage)).toBeVisible();
|
||||
await expect(page).toHaveURL("/sign-in");
|
||||
}
|
||||
|
||||
export async function toggleSamlMode(page: Page) {
|
||||
await page.getByText("Continue with SAML SSO").click();
|
||||
}
|
||||
|
||||
export async function goBackFromSaml(page: Page) {
|
||||
await page.getByText("Back").click();
|
||||
}
|
||||
|
||||
export async function verifySamlModeActive(page: Page) {
|
||||
await expect(page.getByText("Sign in with SAML SSO")).toBeVisible();
|
||||
await expect(page.getByLabel("Password")).not.toBeVisible();
|
||||
await expect(page.getByText("Back")).toBeVisible();
|
||||
}
|
||||
|
||||
export async function verifyNormalModeActive(page: Page) {
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Password")).toBeVisible();
|
||||
}
|
||||
|
||||
export async function logout(page: Page) {
|
||||
await page.getByRole("button", { name: "Sign out" }).click();
|
||||
}
|
||||
|
||||
export async function verifyLogoutSuccess(page: Page) {
|
||||
await expect(page).toHaveURL("/sign-in");
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function verifyLoadingState(page: Page) {
|
||||
const submitButton = page.getByRole("button", { name: "Log in" });
|
||||
await expect(submitButton).toHaveAttribute("aria-disabled", "true");
|
||||
await expect(page.getByText("Loading")).toBeVisible();
|
||||
}
|
||||
|
||||
export async function verifyLoginFormElements(page: Page) {
|
||||
await expect(page).toHaveTitle(/Prowler/);
|
||||
await expect(page.locator('svg[width="300"]')).toBeVisible();
|
||||
|
||||
// Verify form elements
|
||||
await expect(page.getByText("Sign in", { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel("Email")).toBeVisible();
|
||||
await expect(page.getByLabel("Password")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Log in" })).toBeVisible();
|
||||
|
||||
// Verify OAuth buttons
|
||||
await expect(page.getByText("Continue with Google")).toBeVisible();
|
||||
await expect(page.getByText("Continue with Github")).toBeVisible();
|
||||
await expect(page.getByText("Continue with SAML SSO")).toBeVisible();
|
||||
|
||||
// Verify navigation links
|
||||
await expect(page.getByText("Need to create an account?")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Sign up" })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function waitForPageLoad(page: Page) {
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
export async function verifyDashboardRoute(page: Page) {
|
||||
await expect(page).toHaveURL("/");
|
||||
}
|
||||
@@ -34,7 +34,10 @@ export const editScanFormSchema = (currentName: string) =>
|
||||
scanName: z
|
||||
.string()
|
||||
.refine((val) => val === "" || val.length >= 3, {
|
||||
message: "The alias must be empty or have at least 3 characters.",
|
||||
message: "Must be empty or have at least 3 characters.",
|
||||
})
|
||||
.refine((val) => val === "" || val.length <= 32, {
|
||||
message: "Must not exceed 32 characters.",
|
||||
})
|
||||
.refine((val) => val !== currentName, {
|
||||
message: "The new name must be different from the current one.",
|
||||
|
||||
@@ -10,5 +10,5 @@ export const checkSchema = z.object({
|
||||
});
|
||||
|
||||
export const checkDetailsSchema = z.object({
|
||||
id: z.string(),
|
||||
checkId: z.string(),
|
||||
});
|
||||
Reference in new issue
Block a user