From d1424b3c9c417c893e4d19bcc52a03ef19fb81a7 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 15:57:28 +0100 Subject: [PATCH 1/6] fix: resolve breaking changes caused by updated API specs --- actions/auth/auth.ts | 4 ++-- actions/invitations/invitation.ts | 4 ++-- actions/providers/providers.ts | 8 ++++---- actions/scans/scans.ts | 6 +++--- actions/users/users.ts | 2 +- auth.config.ts | 2 +- types/components.ts | 30 +++++++++++++++--------------- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/actions/auth/auth.ts b/actions/auth/auth.ts index 4973e3f8e2..d5ec199b06 100644 --- a/actions/auth/auth.ts +++ b/actions/auth/auth.ts @@ -62,7 +62,7 @@ export const createNewUser = async ( const bodyData = { data: { - type: "User", + type: "users", attributes: { name: formData.name, email: formData.email, @@ -99,7 +99,7 @@ export const getToken = async (formData: z.infer) => { const bodyData = { data: { - type: "Token", + type: "tokens", attributes: { email: formData.email, password: formData.password, diff --git a/actions/invitations/invitation.ts b/actions/invitations/invitation.ts index 7fc99ce33c..654d9fd982 100644 --- a/actions/invitations/invitation.ts +++ b/actions/invitations/invitation.ts @@ -56,7 +56,7 @@ export const sendInvite = async (formData: FormData) => { const body = JSON.stringify({ data: { - type: "Invitation", + type: "invitations", attributes: { email, }, @@ -104,7 +104,7 @@ export const updateInvite = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "Invitation", + type: "invitations", id: invitationId, attributes: { email: invitationEmail, diff --git a/actions/providers/providers.ts b/actions/providers/providers.ts index 29a8a114c7..fc5cd97bb7 100644 --- a/actions/providers/providers.ts +++ b/actions/providers/providers.ts @@ -90,7 +90,7 @@ export const updateProvider = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "Provider", + type: "providers", id: providerId, attributes: { alias: providerAlias, @@ -129,7 +129,7 @@ export const addProvider = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "Provider", + type: "providers", attributes: { provider: providerType, uid: providerUid, @@ -209,7 +209,7 @@ export const addCredentialsProvider = async (formData: FormData) => { const bodyData = { data: { - type: "ProviderSecret", + type: "provider-secrets", attributes: { secret_type: secretType, secret, @@ -219,7 +219,7 @@ export const addCredentialsProvider = async (formData: FormData) => { provider: { data: { id: providerId, - type: "Provider", + type: "providers", }, }, }, diff --git a/actions/scans/scans.ts b/actions/scans/scans.ts index 866918cf86..367446daaa 100644 --- a/actions/scans/scans.ts +++ b/actions/scans/scans.ts @@ -91,14 +91,14 @@ export const scanOnDemand = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "Scan", + type: "scans", attributes: { name: scanName, }, relationships: { provider: { data: { - type: "Provider", + type: "providers", id: providerId, }, }, @@ -136,7 +136,7 @@ export const updateScan = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "Scan", + type: "scans", id: scanId, attributes: { name: scanName, diff --git a/actions/users/users.ts b/actions/users/users.ts index 903d0be120..30a5b26d1f 100644 --- a/actions/users/users.ts +++ b/actions/users/users.ts @@ -69,7 +69,7 @@ export const updateUser = async (formData: FormData) => { }, body: JSON.stringify({ data: { - type: "User", + type: "users", id: userId, attributes: { name: userName, diff --git a/auth.config.ts b/auth.config.ts index 4639f8bf37..56c44a14e5 100644 --- a/auth.config.ts +++ b/auth.config.ts @@ -16,7 +16,7 @@ const refreshAccessToken = async (token: JwtPayload) => { const bodyData = { data: { - type: "TokenRefresh", + type: "tokens-refresh", attributes: { refresh: (token as any).refreshToken, }, diff --git a/types/components.ts b/types/components.ts index 2299d0bffe..52b553cce0 100644 --- a/types/components.ts +++ b/types/components.ts @@ -102,7 +102,7 @@ export interface ApiError { } export interface InvitationProps { - type: "Invitation"; + type: "invitations"; id: string; attributes: { inserted_at: string; @@ -115,7 +115,7 @@ export interface InvitationProps { relationships: { inviter: { data: { - type: "User"; + type: "users"; id: string; }; }; @@ -126,7 +126,7 @@ export interface InvitationProps { } export interface UserProps { - type: "User"; + type: "users"; id: string; attributes: { name: string; @@ -140,7 +140,7 @@ export interface UserProps { count: number; }; data: Array<{ - type: "Membership"; + type: "memberships"; id: string; }>; }; @@ -175,7 +175,7 @@ export interface ProviderProps { } export interface ScanProps { - type: "Scan"; + type: "scans"; id: string; attributes: { name: string; @@ -203,20 +203,20 @@ export interface ScanProps { provider: { data: { id: string; - type: "Provider"; + type: "providers"; }; }; task: { data: { id: string; - type: "Task"; + type: "tasks"; }; }; }; } export interface FindingProps { - type: "Finding"; + type: "findings"; id: string; attributes: { uid: string; @@ -264,13 +264,13 @@ export interface FindingProps { relationships: { resources: { data: { - type: "Resource"; + type: "resources"; id: string; }[]; }; scan: { data: { - type: "Scan"; + type: "scans"; id: string; }; attributes: { @@ -290,7 +290,7 @@ export interface FindingProps { }; resource: { data: { - type: "Resource"; + type: "resources"; id: string; }[]; id: string; @@ -307,7 +307,7 @@ export interface FindingProps { relationships: { provider: { data: { - type: "Provider"; + type: "providers"; id: string; }; }; @@ -316,7 +316,7 @@ export interface FindingProps { count: number; }; data: { - type: "Finding"; + type: "findings"; id: string; }[]; }; @@ -327,7 +327,7 @@ export interface FindingProps { }; provider: { data: { - type: "Provider"; + type: "providers"; id: string; }; attributes: { @@ -344,7 +344,7 @@ export interface FindingProps { relationships: { secret: { data: { - type: "ProviderSecret"; + type: "provider-secrets"; id: string; }; }; From e92bbffc532fc405bc139105422f581de875cfca Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Sat, 16 Nov 2024 21:13:41 +0100 Subject: [PATCH 2/6] chore: delete old dummy data for compliances dashboard --- actions/compliance.ts | 39 ---- app/api/compliance/route.ts | 10 - dataCompliance.json | 438 ------------------------------------ 3 files changed, 487 deletions(-) delete mode 100644 actions/compliance.ts delete mode 100644 app/api/compliance/route.ts delete mode 100644 dataCompliance.json diff --git a/actions/compliance.ts b/actions/compliance.ts deleted file mode 100644 index 304b826077..0000000000 --- a/actions/compliance.ts +++ /dev/null @@ -1,39 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { redirect } from "next/navigation"; - -import { parseStringify } from "@/lib"; - -export const getCompliance = async ({ page = 1 }) => { - if (isNaN(Number(page)) || page < 1) redirect("/compliance"); - const keyServer = process.env.SITE_URL; - - try { - const compliance = await fetch( - `${keyServer}/api/compliance?page%5Bnumber%5D=${page}`, - ); - const data = await compliance.json(); - const parsedData = parseStringify(data); - revalidatePath("/compliance"); - return parsedData; - } catch (error) { - console.error("Error fetching Compliance:", error); - return undefined; - } -}; - -export const getErrorMessage = (error: unknown): string => { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else if (error && typeof error === "object" && "message" in error) { - message = String(error.message); - } else if (typeof error === "string") { - message = error; - } else { - message = "Oops! Something went wrong."; - } - return message; -}; diff --git a/app/api/compliance/route.ts b/app/api/compliance/route.ts deleted file mode 100644 index b2e4a01d9f..0000000000 --- a/app/api/compliance/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from "next/server"; - -import data from "../../../dataCompliance.json"; - -export async function GET() { - // Simulate fetching data with a delay - await new Promise((resolve) => setTimeout(resolve, 2000)); - - return NextResponse.json({ compliance: data }); -} diff --git a/dataCompliance.json b/dataCompliance.json deleted file mode 100644 index a1bdd62831..0000000000 --- a/dataCompliance.json +++ /dev/null @@ -1,438 +0,0 @@ -{ - "links": { - "first": "http://localhost:8080/api/v1/compliance?page%5Bnumber%5D=1", - "last": "http://localhost:8080/api/v1/compliance?page%5Bnumber%5D=1", - "next": null, - "prev": null - }, - "data": [ - { - "id": "001", - "attributes": { - "title": "AWS Account Security Onboarding", - "passingRequirements": 28, - "totalRequirements": 83, - "regions": ["us-east-1", "us-east-2"] - }, - "lastScan": { - "attributes": { - "passingRequirements": 28, - "totalRequirements": 83 - } - } - }, - { - "id": "002", - "attributes": { - "title": "AWS Audit Manager Control Tower Guardrails", - "passingRequirements": 10, - "totalRequirements": 14, - "regions": ["us-west-1", "us-west-2"] - }, - "lastScan": { - "attributes": { - "passingRequirements": 14, - "totalRequirements": 14 - } - } - }, - { - "id": "003", - "attributes": { - "title": "AWS Foundational Security Best Practices", - "passingRequirements": 22, - "totalRequirements": 37, - "regions": ["eu-west-1", "eu-west-2"] - }, - "lastScan": { - "attributes": { - "passingRequirements": 22, - "totalRequirements": 37 - } - } - }, - { - "id": "004", - "attributes": { - "title": "AWS Foundational Technical Review", - "passingRequirements": 3, - "totalRequirements": 45, - "regions": ["ap-southeast-1", "ap-southeast-2"] - }, - "lastScan": { - "attributes": { - "passingRequirements": 25, - "totalRequirements": 45 - } - } - }, - { - "id": "005", - "attributes": { - "title": "AWS Well Architected Framework Reliability Pillar", - "passingRequirements": 2, - "totalRequirements": 3, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 0, - "totalRequirements": 3 - } - } - }, - { - "id": "006", - "attributes": { - "title": "AWS Well Architected Framework Security Pillar", - "passingRequirements": 19, - "totalRequirements": 57, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 24, - "totalRequirements": 57 - } - } - }, - { - "id": "007", - "attributes": { - "title": "CIS 1.4", - "passingRequirements": 43, - "totalRequirements": 58, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 40, - "totalRequirements": 58 - } - } - }, - { - "id": "008", - "attributes": { - "title": "CIS 1.5", - "passingRequirements": 48, - "totalRequirements": 63, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 50, - "totalRequirements": 63 - } - } - }, - { - "id": "009", - "attributes": { - "title": "CIS 2.0", - "passingRequirements": 47, - "totalRequirements": 64, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 64, - "totalRequirements": 64 - } - } - }, - { - "id": "010", - "attributes": { - "title": "CIS 3.0", - "passingRequirements": 45, - "totalRequirements": 62, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 48, - "totalRequirements": 62 - } - } - }, - { - "id": "011", - "attributes": { - "title": "CISA", - "passingRequirements": 4, - "totalRequirements": 16, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 4, - "totalRequirements": 16 - } - } - }, - { - "id": "012", - "attributes": { - "title": "ENS RD2022 Categoría ALTA", - "passingRequirements": 89, - "totalRequirements": 189, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 82, - "totalRequirements": 189 - } - } - }, - { - "id": "013", - "attributes": { - "title": "FFIEC", - "passingRequirements": 8, - "totalRequirements": 44, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 40, - "totalRequirements": 44 - } - } - }, - { - "id": "014", - "attributes": { - "title": "FedRAMP Low Revision 4", - "passingRequirements": 4, - "totalRequirements": 18, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 4, - "totalRequirements": 18 - } - } - }, - { - "id": "015", - "attributes": { - "title": "FedRamp Moderate Revision 4", - "passingRequirements": 11, - "totalRequirements": 64, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 28, - "totalRequirements": 64 - } - } - }, - { - "id": "016", - "attributes": { - "title": "GDPR", - "passingRequirements": 0, - "totalRequirements": 3, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 0, - "totalRequirements": 3 - } - } - }, - { - "id": "017", - "attributes": { - "title": "GxP 21 CFR Part 11", - "passingRequirements": 1, - "totalRequirements": 11, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 11, - "totalRequirements": 11 - } - } - }, - { - "id": "018", - "attributes": { - "title": "GxP EU Annex 11", - "passingRequirements": 5, - "totalRequirements": 14, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 5, - "totalRequirements": 14 - } - } - }, - { - "id": "019", - "attributes": { - "title": "HIPAA", - "passingRequirements": 2, - "totalRequirements": 32, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 9, - "totalRequirements": 32 - } - } - }, - { - "id": "020", - "attributes": { - "title": "ISO27001 2013", - "passingRequirements": 55, - "totalRequirements": 79, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 66, - "totalRequirements": 79 - } - } - }, - { - "id": "021", - "attributes": { - "title": "MITRE ATTACK", - "passingRequirements": 7, - "totalRequirements": 46, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 19, - "totalRequirements": 46 - } - } - }, - { - "id": "022", - "attributes": { - "title": "NIST 800 171 Revision 2", - "passingRequirements": 3, - "totalRequirements": 50, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 3, - "totalRequirements": 50 - } - } - }, - { - "id": "023", - "attributes": { - "title": "NIST 800 53 Revision 4", - "passingRequirements": 21, - "totalRequirements": 64, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 21, - "totalRequirements": 64 - } - } - }, - { - "id": "024", - "attributes": { - "title": "NIST 800 53 Revision 5", - "passingRequirements": 75, - "totalRequirements": 288, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 75, - "totalRequirements": 288 - } - } - }, - { - "id": "025", - "attributes": { - "title": "NIST CSF 1.1", - "passingRequirements": 15, - "totalRequirements": 56, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 46, - "totalRequirements": 56 - } - } - }, - { - "id": "026", - "attributes": { - "title": "PCI 3.2.1", - "passingRequirements": 11, - "totalRequirements": 19, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 19, - "totalRequirements": 19 - } - } - }, - { - "id": "027", - "attributes": { - "title": "RBI Cyber Security Framework", - "passingRequirements": 3, - "totalRequirements": 9, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 3, - "totalRequirements": 9 - } - } - }, - { - "id": "028", - "attributes": { - "title": "SOC2", - "passingRequirements": 7, - "totalRequirements": 56, - "regions": [] - }, - "lastScan": { - "attributes": { - "passingRequirements": 7, - "totalRequirements": 56 - } - } - } - ], - "meta": { - "pagination": { - "page": 1, - "pages": 1, - "count": 28 - }, - "version": "v1" - } -} From eb40369c30b7a2f6e1e6fdd6ab62eff617a8804b Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 18 Nov 2024 07:45:19 +0100 Subject: [PATCH 3/6] chore: render an message if there is no data for compliances --- actions/compliances/compliances.ts | 34 ++++++++++++++++ actions/compliances/index.ts | 1 + actions/invitations/index.ts | 1 + actions/invitations/invitation.ts | 2 + app/(prowler)/compliance/page.tsx | 64 ++++++++++++++++-------------- types/components.ts | 47 ++++++++++++++++++++++ 6 files changed, 120 insertions(+), 29 deletions(-) create mode 100644 actions/compliances/compliances.ts create mode 100644 actions/compliances/index.ts create mode 100644 actions/invitations/index.ts diff --git a/actions/compliances/compliances.ts b/actions/compliances/compliances.ts new file mode 100644 index 0000000000..6b254acff0 --- /dev/null +++ b/actions/compliances/compliances.ts @@ -0,0 +1,34 @@ +"use server"; +import { revalidatePath } from "next/cache"; + +import { auth } from "@/auth.config"; +import { parseStringify } from "@/lib"; + +export const getCompliancesOverview = async ({ + scanId, +}: { + scanId: string; +}) => { + const session = await auth(); + + const keyServer = process.env.API_BASE_URL; + const url = new URL( + `${keyServer}/compliance-overviews?filter[scan_id]=${scanId}`, + ); + try { + const compliances = await fetch(url.toString(), { + headers: { + Accept: "application/vnd.api+json", + Authorization: `Bearer ${session?.accessToken}`, + }, + }); + const data = await compliances.json(); + const parsedData = parseStringify(data); + revalidatePath("/compliance"); + return parsedData; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Error fetching providers:", error); + return undefined; + } +}; diff --git a/actions/compliances/index.ts b/actions/compliances/index.ts new file mode 100644 index 0000000000..ba9df7e860 --- /dev/null +++ b/actions/compliances/index.ts @@ -0,0 +1 @@ +export * from "./compliances"; diff --git a/actions/invitations/index.ts b/actions/invitations/index.ts new file mode 100644 index 0000000000..addd041884 --- /dev/null +++ b/actions/invitations/index.ts @@ -0,0 +1 @@ +export * from "./invitation"; diff --git a/actions/invitations/invitation.ts b/actions/invitations/invitation.ts index 654d9fd982..56aefc74e1 100644 --- a/actions/invitations/invitation.ts +++ b/actions/invitations/invitation.ts @@ -42,6 +42,7 @@ export const getInvitations = async ({ revalidatePath("/invitations"); return parsedData; } catch (error) { + // eslint-disable-next-line no-console console.error("Error fetching invitations:", error); return undefined; } @@ -117,6 +118,7 @@ export const updateInvite = async (formData: FormData) => { revalidatePath("/invitations"); return parseStringify(data); } catch (error) { + // eslint-disable-next-line no-console console.error("Error updating invitation:", error); return { error: getErrorMessage(error), diff --git a/app/(prowler)/compliance/page.tsx b/app/(prowler)/compliance/page.tsx index 6cc1c5c11e..7e47723441 100644 --- a/app/(prowler)/compliance/page.tsx +++ b/app/(prowler)/compliance/page.tsx @@ -1,15 +1,14 @@ import { Spacer } from "@nextui-org/react"; -import { redirect } from "next/navigation"; import { Suspense } from "react"; -import { getCompliance } from "@/actions/compliance"; +import { getCompliancesOverview } from "@/actions/compliances"; import { ComplianceCard, ComplianceSkeletonGrid, } from "@/components/compliance"; import { FilterControls } from "@/components/filters"; import { Header } from "@/components/ui"; -import { SearchParamsProps } from "@/types"; +import { ComplianceOverviewData, SearchParamsProps } from "@/types"; export default async function Compliance({ searchParams, @@ -23,41 +22,48 @@ export default async function Compliance({
- + }> - + ); } -const SSRComplianceGrid = async ({ - searchParams, -}: { - searchParams: SearchParamsProps; -}) => { - const page = parseInt(searchParams.page?.toString() || "1", 10); - const compliancesData = await getCompliance({ page }); - const [compliances] = await Promise.all([compliancesData]); +const SSRComplianceGrid = async () => { + // const scanId = "01929f57-c0ee-7553-be0b-cbde006fb6f7"; + const scanId = "0193358c-bd7f-7eec-b13a-2d4a648b8df"; + const compliancesData = await getCompliancesOverview({ scanId }); + console.log(compliancesData, "compliancesData"); - if (compliances?.errors) redirect("/compliance"); + if (compliancesData?.errors?.length > 0) { + return ( +
+
There is no compliance data.
+
+ ); + } return ( -
- {compliances.compliance?.data.map((compliance: any) => ( - - ))} +
+ {compliancesData?.data?.map((compliance: ComplianceOverviewData) => { + const { attributes } = compliance; + const { + framework, + requirements_status: { passed, total }, + } = attributes; + + return ( + + ); + })}
); }; diff --git a/types/components.ts b/types/components.ts index 52b553cce0..7ec383d408 100644 --- a/types/components.ts +++ b/types/components.ts @@ -100,6 +100,53 @@ export interface ApiError { }; code: string; } +export interface CompliancesOverview { + links: { + first: string; + last: string; + next: string | null; + prev: string | null; + }; + data: ComplianceOverviewData[]; + meta: { + pagination: { + page: number; + pages: number; + count: number; + }; + version: string; + }; +} + +export interface ComplianceOverviewData { + type: "compliance-overviews"; + id: string; + attributes: { + inserted_at: string; + compliance_id: string; + framework: string; + version: string; + requirements_status: { + passed: number; + failed: number; + manual: number; + total: number; + }; + region: string; + provider_type: string; + }; + relationships: { + scan: { + data: { + type: "scans"; + id: string; + }; + }; + }; + links: { + self: string; + }; +} export interface InvitationProps { type: "invitations"; From 783db5c3dcd4e5c565df875710d8890674deedf8 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 18 Nov 2024 08:21:54 +0100 Subject: [PATCH 4/6] feat: allow compliance data selection by choosing a scan --- app/(prowler)/compliance/page.tsx | 23 +++++--- ...ComplianceCard.tsx => compliance-card.tsx} | 0 ...nGrid.tsx => compliance-skeleton-grid.tsx} | 0 .../data-compliance/data-compliance.tsx | 31 +++++++++++ .../compliance/data-compliance/index.ts | 2 + .../select-scan-compliance-data.tsx | 52 +++++++++++++++++++ components/compliance/index.ts | 4 +- 7 files changed, 103 insertions(+), 9 deletions(-) rename components/compliance/{ComplianceCard.tsx => compliance-card.tsx} (100%) rename components/compliance/{ComplianceSkeletonGrid.tsx => compliance-skeleton-grid.tsx} (100%) create mode 100644 components/compliance/data-compliance/data-compliance.tsx create mode 100644 components/compliance/data-compliance/index.ts create mode 100644 components/compliance/data-compliance/select-scan-compliance-data.tsx diff --git a/app/(prowler)/compliance/page.tsx b/app/(prowler)/compliance/page.tsx index 7e47723441..8d3f8315f1 100644 --- a/app/(prowler)/compliance/page.tsx +++ b/app/(prowler)/compliance/page.tsx @@ -2,10 +2,12 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getCompliancesOverview } from "@/actions/compliances"; +import { getScans } from "@/actions/scans"; import { ComplianceCard, ComplianceSkeletonGrid, } from "@/components/compliance"; +import { DataCompliance } from "@/components/compliance/data-compliance"; import { FilterControls } from "@/components/filters"; import { Header } from "@/components/ui"; import { ComplianceOverviewData, SearchParamsProps } from "@/types"; @@ -15,26 +17,33 @@ export default async function Compliance({ }: { searchParams: SearchParamsProps; }) { - const searchParamsKey = JSON.stringify(searchParams || {}); + const scansData = await getScans({}); + const scanList = scansData?.data.map((scan: any) => ({ + id: scan.id, + name: scan.attributes.name || "Unnamed Scan", + state: scan.attributes.state, + progress: scan.attributes.progress, + })); + const selectedScanId = searchParams.scanId || scanList[0]?.id; return ( <>
+
+ +
- }> - + }> + ); } -const SSRComplianceGrid = async () => { - // const scanId = "01929f57-c0ee-7553-be0b-cbde006fb6f7"; - const scanId = "0193358c-bd7f-7eec-b13a-2d4a648b8df"; +const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => { const compliancesData = await getCompliancesOverview({ scanId }); - console.log(compliancesData, "compliancesData"); if (compliancesData?.errors?.length > 0) { return ( diff --git a/components/compliance/ComplianceCard.tsx b/components/compliance/compliance-card.tsx similarity index 100% rename from components/compliance/ComplianceCard.tsx rename to components/compliance/compliance-card.tsx diff --git a/components/compliance/ComplianceSkeletonGrid.tsx b/components/compliance/compliance-skeleton-grid.tsx similarity index 100% rename from components/compliance/ComplianceSkeletonGrid.tsx rename to components/compliance/compliance-skeleton-grid.tsx diff --git a/components/compliance/data-compliance/data-compliance.tsx b/components/compliance/data-compliance/data-compliance.tsx new file mode 100644 index 0000000000..52484f6bd7 --- /dev/null +++ b/components/compliance/data-compliance/data-compliance.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; + +import { SelectScanComplianceData } from "@/components/compliance/data-compliance"; + +interface DataComplianceProps { + scans: { id: string; name: string; state: string; progress: number }[]; +} + +export const DataCompliance = ({ scans }: DataComplianceProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const selectedScanId = searchParams.get("scanId") || scans[0]?.id; + + const handleSelectionChange = (selectedKey: string) => { + router.push(`?scanId=${selectedKey}`); + }; + + return ( +
+
+ +
+
+ ); +}; diff --git a/components/compliance/data-compliance/index.ts b/components/compliance/data-compliance/index.ts new file mode 100644 index 0000000000..14f430df04 --- /dev/null +++ b/components/compliance/data-compliance/index.ts @@ -0,0 +1,2 @@ +export * from "./data-compliance"; +export * from "./select-scan-compliance-data"; diff --git a/components/compliance/data-compliance/select-scan-compliance-data.tsx b/components/compliance/data-compliance/select-scan-compliance-data.tsx new file mode 100644 index 0000000000..fb1189019c --- /dev/null +++ b/components/compliance/data-compliance/select-scan-compliance-data.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { Select, SelectItem } from "@nextui-org/react"; + +interface SelectScanComplianceDataProps { + scans: { id: string; name: string; state: string; progress: number }[]; + selectedScanId: string; + onSelectionChange: (selectedKey: string) => void; +} + +export const SelectScanComplianceData = ({ + scans, + selectedScanId, + onSelectionChange, +}: SelectScanComplianceDataProps) => { + return ( + + ); +}; diff --git a/components/compliance/index.ts b/components/compliance/index.ts index 6f025f2ca5..bb1b06d995 100644 --- a/components/compliance/index.ts +++ b/components/compliance/index.ts @@ -1,2 +1,2 @@ -export * from "./ComplianceCard"; -export * from "./ComplianceSkeletonGrid"; +export * from "./compliance-card"; +export * from "./compliance-skeleton-grid"; From 223073e3df1aee84b2eb748c718556620073723f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 18 Nov 2024 09:07:07 +0100 Subject: [PATCH 5/6] feat: allow compliance data selection by choosing a scan --- app/(prowler)/compliance/page.tsx | 14 +++++++++++++- .../compliance/data-compliance/data-compliance.tsx | 9 ++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/(prowler)/compliance/page.tsx b/app/(prowler)/compliance/page.tsx index 8d3f8315f1..68a9a89dbe 100644 --- a/app/(prowler)/compliance/page.tsx +++ b/app/(prowler)/compliance/page.tsx @@ -45,10 +45,22 @@ export default async function Compliance({ const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => { const compliancesData = await getCompliancesOverview({ scanId }); + // Check if the response contains no data + if (!compliancesData || compliancesData?.data?.length === 0) { + return ( +
+
+ No compliance data available for the selected scan. +
+
+ ); + } + + // Handle errors returned by the API if (compliancesData?.errors?.length > 0) { return (
-
There is no compliance data.
+
Provide a valid scan ID.
); } diff --git a/components/compliance/data-compliance/data-compliance.tsx b/components/compliance/data-compliance/data-compliance.tsx index 52484f6bd7..67e45f9ff4 100644 --- a/components/compliance/data-compliance/data-compliance.tsx +++ b/components/compliance/data-compliance/data-compliance.tsx @@ -11,7 +11,14 @@ interface DataComplianceProps { export const DataCompliance = ({ scans }: DataComplianceProps) => { const router = useRouter(); const searchParams = useSearchParams(); - const selectedScanId = searchParams.get("scanId") || scans[0]?.id; + const scanIdParam = searchParams.get("scanId"); + const selectedScanId = + scanIdParam === "undefined" || !scanIdParam ? scans[0]?.id : scanIdParam; + + if (scanIdParam === "undefined") { + router.replace("/compliance"); + return null; + } const handleSelectionChange = (selectedKey: string) => { router.push(`?scanId=${selectedKey}`); From 985efc67cc37b406de893d5887923cc05b61f261 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Mon, 18 Nov 2024 14:05:36 +0100 Subject: [PATCH 6/6] feat: first iteration of compliance dashboard is working --- actions/compliances/compliances.ts | 15 +++- app/(prowler)/compliance/page.tsx | 48 +++++++++--- .../data-compliance/data-compliance.tsx | 74 ++++++++++++++++--- .../select-scan-compliance-data.tsx | 2 - 4 files changed, 112 insertions(+), 27 deletions(-) diff --git a/actions/compliances/compliances.ts b/actions/compliances/compliances.ts index 6b254acff0..e9c8814c31 100644 --- a/actions/compliances/compliances.ts +++ b/actions/compliances/compliances.ts @@ -6,15 +6,23 @@ import { parseStringify } from "@/lib"; export const getCompliancesOverview = async ({ scanId, + region, }: { scanId: string; + region?: string | string[]; }) => { const session = await auth(); const keyServer = process.env.API_BASE_URL; - const url = new URL( - `${keyServer}/compliance-overviews?filter[scan_id]=${scanId}`, - ); + const url = new URL(`${keyServer}/compliance-overviews`); + + if (scanId) url.searchParams.append("filter[scan_id]", scanId); + + if (region) { + const regionValue = Array.isArray(region) ? region.join(",") : region; + url.searchParams.append("filter[region__in]", regionValue); + } + try { const compliances = await fetch(url.toString(), { headers: { @@ -24,6 +32,7 @@ export const getCompliancesOverview = async ({ }); const data = await compliances.json(); const parsedData = parseStringify(data); + revalidatePath("/compliance"); return parsedData; } catch (error) { diff --git a/app/(prowler)/compliance/page.tsx b/app/(prowler)/compliance/page.tsx index 68a9a89dbe..08368c69da 100644 --- a/app/(prowler)/compliance/page.tsx +++ b/app/(prowler)/compliance/page.tsx @@ -8,7 +8,6 @@ import { ComplianceSkeletonGrid, } from "@/components/compliance"; import { DataCompliance } from "@/components/compliance/data-compliance"; -import { FilterControls } from "@/components/filters"; import { Header } from "@/components/ui"; import { ComplianceOverviewData, SearchParamsProps } from "@/types"; @@ -24,26 +23,53 @@ export default async function Compliance({ state: scan.attributes.state, progress: scan.attributes.progress, })); + const selectedScanId = searchParams.scanId || scanList[0]?.id; + // Fetch compliance data for regions + const compliancesData = await getCompliancesOverview({ + scanId: selectedScanId, + }); + + // Extract unique regions + const regions = compliancesData?.data + ? Array.from( + new Set( + compliancesData.data.map( + (compliance: ComplianceOverviewData) => + compliance.attributes.region as string, + ), + ), + ) + : []; + return ( <>
-
- -
- - + + }> - + ); } -const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => { - const compliancesData = await getCompliancesOverview({ scanId }); +const SSRComplianceGrid = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const scanId = searchParams.scanId?.toString() || ""; + + const regionFilter = searchParams["filter[region__in]"]?.toString() || ""; + + // Fetch compliance data + const compliancesData = await getCompliancesOverview({ + scanId, + region: regionFilter, + }); // Check if the response contains no data if (!compliancesData || compliancesData?.data?.length === 0) { @@ -66,8 +92,8 @@ const SSRComplianceGrid = async ({ scanId }: { scanId: string }) => { } return ( -
- {compliancesData?.data?.map((compliance: ComplianceOverviewData) => { +
+ {compliancesData.data.map((compliance: ComplianceOverviewData) => { const { attributes } = compliance; const { framework, diff --git a/components/compliance/data-compliance/data-compliance.tsx b/components/compliance/data-compliance/data-compliance.tsx index 67e45f9ff4..10fcc44f6b 100644 --- a/components/compliance/data-compliance/data-compliance.tsx +++ b/components/compliance/data-compliance/data-compliance.tsx @@ -1,37 +1,89 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; import { SelectScanComplianceData } from "@/components/compliance/data-compliance"; +import { CrossIcon } from "@/components/icons"; +import { CustomButton, CustomDropdownFilter } from "@/components/ui/custom"; interface DataComplianceProps { scans: { id: string; name: string; state: string; progress: number }[]; + regions: string[]; } -export const DataCompliance = ({ scans }: DataComplianceProps) => { +export const DataCompliance = ({ scans, regions }: DataComplianceProps) => { const router = useRouter(); const searchParams = useSearchParams(); + const [showClearButton, setShowClearButton] = useState(false); const scanIdParam = searchParams.get("scanId"); - const selectedScanId = - scanIdParam === "undefined" || !scanIdParam ? scans[0]?.id : scanIdParam; + const selectedScanId = scanIdParam || scans[0]?.id; - if (scanIdParam === "undefined") { - router.replace("/compliance"); - return null; - } - - const handleSelectionChange = (selectedKey: string) => { - router.push(`?scanId=${selectedKey}`); + useEffect(() => { + const hasFilters = Array.from(searchParams.keys()).some( + (key) => key.startsWith("filter[") || key === "sort", + ); + setShowClearButton(hasFilters); + }, [searchParams]); + const handleScanChange = (selectedKey: string) => { + const params = new URLSearchParams(searchParams); + params.set("scanId", selectedKey); + router.push(`?${params.toString()}`); }; + const pushDropdownFilter = useCallback( + (key: string, values: string[]) => { + const params = new URLSearchParams(searchParams); + const filterKey = `filter[${key}]`; + + if (values.length === 0) { + params.delete(filterKey); + } else { + params.set(filterKey, values.join(",")); + } + + router.push(`?${params.toString()}`); + }, + [router, searchParams], + ); + const clearAllFilters = useCallback(() => { + const params = new URLSearchParams(searchParams.toString()); + Array.from(params.keys()).forEach((key) => { + if (key.startsWith("filter[") || key === "sort") { + params.delete(key); + } + }); + router.push(`?${params.toString()}`, { scroll: false }); + }, [router, searchParams]); return (
+ + {showClearButton && ( + } + radius="sm" + > + Reset + + )}
); diff --git a/components/compliance/data-compliance/select-scan-compliance-data.tsx b/components/compliance/data-compliance/select-scan-compliance-data.tsx index fb1189019c..5ebc36515d 100644 --- a/components/compliance/data-compliance/select-scan-compliance-data.tsx +++ b/components/compliance/data-compliance/select-scan-compliance-data.tsx @@ -1,5 +1,3 @@ -"use client"; - import { Select, SelectItem } from "@nextui-org/react"; interface SelectScanComplianceDataProps {